webspresso 0.0.8 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "bin/",
34
34
  "src/",
35
35
  "utils/",
36
- "core/"
36
+ "core/",
37
+ "plugins/"
37
38
  ],
38
39
  "dependencies": {
39
40
  "commander": "^11.1.0",
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Webspresso Analytics Plugin
3
+ * Adds tracking scripts and verification meta tags for Google, Yandex, and Bing
4
+ */
5
+
6
+ /**
7
+ * Generate Google Analytics (gtag.js) script
8
+ */
9
+ function generateGtagScript(config) {
10
+ if (!config || !config.measurementId) return '';
11
+
12
+ const { measurementId, adsId } = config;
13
+
14
+ let script = `<!-- Google Analytics -->
15
+ <script async src="https://www.googletagmanager.com/gtag/js?id=${measurementId}"></script>
16
+ <script>
17
+ window.dataLayer = window.dataLayer || [];
18
+ function gtag(){dataLayer.push(arguments);}
19
+ gtag('js', new Date());
20
+ gtag('config', '${measurementId}');`;
21
+
22
+ if (adsId) {
23
+ script += `
24
+ gtag('config', '${adsId}');`;
25
+ }
26
+
27
+ script += `
28
+ </script>`;
29
+
30
+ return script;
31
+ }
32
+
33
+ /**
34
+ * Generate Google Tag Manager script
35
+ */
36
+ function generateGtmScript(config) {
37
+ if (!config || !config.containerId) return '';
38
+
39
+ const { containerId } = config;
40
+
41
+ return `<!-- Google Tag Manager -->
42
+ <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
43
+ new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
44
+ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
45
+ 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
46
+ })(window,document,'script','dataLayer','${containerId}');</script>`;
47
+ }
48
+
49
+ /**
50
+ * Generate Google Tag Manager noscript fallback
51
+ */
52
+ function generateGtmNoscript(config) {
53
+ if (!config || !config.containerId) return '';
54
+
55
+ return `<!-- Google Tag Manager (noscript) -->
56
+ <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=${config.containerId}"
57
+ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>`;
58
+ }
59
+
60
+ /**
61
+ * Generate Yandex.Metrika script
62
+ */
63
+ function generateYandexScript(config) {
64
+ if (!config || !config.counterId) return '';
65
+
66
+ const { counterId, clickmap = true, trackLinks = true, accurateTrackBounce = true, webvisor = false } = config;
67
+
68
+ return `<!-- Yandex.Metrika -->
69
+ <script>
70
+ (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
71
+ m[i].l=1*new Date();
72
+ for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
73
+ k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
74
+ (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
75
+
76
+ ym(${counterId}, "init", {
77
+ clickmap:${clickmap},
78
+ trackLinks:${trackLinks},
79
+ accurateTrackBounce:${accurateTrackBounce}${webvisor ? ',\n webvisor:true' : ''}
80
+ });
81
+ </script>
82
+ <noscript><div><img src="https://mc.yandex.ru/watch/${counterId}" style="position:absolute; left:-9999px;" alt="" /></div></noscript>`;
83
+ }
84
+
85
+ /**
86
+ * Generate Microsoft/Bing UET script
87
+ */
88
+ function generateBingScript(config) {
89
+ if (!config || !config.uetId) return '';
90
+
91
+ const { uetId } = config;
92
+
93
+ return `<!-- Microsoft UET -->
94
+ <script>
95
+ (function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"${uetId}",enableAutoSpaTracking:true};
96
+ o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){
97
+ var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)
98
+ })(window,document,"script","//bat.bing.com/bat.js","uetq");
99
+ </script>`;
100
+ }
101
+
102
+ /**
103
+ * Generate Facebook Pixel script
104
+ */
105
+ function generateFacebookScript(config) {
106
+ if (!config || !config.pixelId) return '';
107
+
108
+ const { pixelId } = config;
109
+
110
+ return `<!-- Facebook Pixel -->
111
+ <script>
112
+ !function(f,b,e,v,n,t,s)
113
+ {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
114
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
115
+ if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
116
+ n.queue=[];t=b.createElement(e);t.async=!0;
117
+ t.src=v;s=b.getElementsByTagName(e)[0];
118
+ s.parentNode.insertBefore(t,s)}(window, document,'script',
119
+ 'https://connect.facebook.net/en_US/fbevents.js');
120
+ fbq('init', '${pixelId}');
121
+ fbq('track', 'PageView');
122
+ </script>
123
+ <noscript><img height="1" width="1" style="display:none"
124
+ src="https://www.facebook.com/tr?id=${pixelId}&ev=PageView&noscript=1"/></noscript>`;
125
+ }
126
+
127
+ /**
128
+ * Generate verification meta tags
129
+ */
130
+ function generateVerificationTags(options) {
131
+ const tags = [];
132
+
133
+ if (options.google?.verificationCode) {
134
+ tags.push(`<meta name="google-site-verification" content="${options.google.verificationCode}">`);
135
+ }
136
+
137
+ if (options.yandex?.verificationCode) {
138
+ tags.push(`<meta name="yandex-verification" content="${options.yandex.verificationCode}">`);
139
+ }
140
+
141
+ if (options.bing?.verificationCode) {
142
+ tags.push(`<meta name="msvalidate.01" content="${options.bing.verificationCode}">`);
143
+ }
144
+
145
+ if (options.facebook?.domainVerification) {
146
+ tags.push(`<meta name="facebook-domain-verification" content="${options.facebook.domainVerification}">`);
147
+ }
148
+
149
+ if (options.pinterest?.verificationCode) {
150
+ tags.push(`<meta name="p:domain_verify" content="${options.pinterest.verificationCode}">`);
151
+ }
152
+
153
+ return tags.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Create the analytics plugin
158
+ * @param {Object} options - Plugin options
159
+ * @param {Object} options.google - Google Analytics config
160
+ * @param {string} options.google.measurementId - GA4 Measurement ID (G-XXXXXXXXXX)
161
+ * @param {string} options.google.adsId - Google Ads ID (AW-XXXXXXXXXX)
162
+ * @param {string} options.google.verificationCode - Search Console verification
163
+ * @param {Object} options.gtm - Google Tag Manager config
164
+ * @param {string} options.gtm.containerId - GTM Container ID (GTM-XXXXXXX)
165
+ * @param {Object} options.yandex - Yandex.Metrika config
166
+ * @param {string} options.yandex.counterId - Yandex counter ID
167
+ * @param {string} options.yandex.verificationCode - Yandex Webmaster verification
168
+ * @param {boolean} options.yandex.webvisor - Enable Webvisor
169
+ * @param {Object} options.bing - Microsoft/Bing config
170
+ * @param {string} options.bing.uetId - UET tag ID
171
+ * @param {string} options.bing.verificationCode - Bing Webmaster verification
172
+ * @param {Object} options.facebook - Facebook/Meta config
173
+ * @param {string} options.facebook.pixelId - Facebook Pixel ID
174
+ * @param {string} options.facebook.domainVerification - Domain verification code
175
+ */
176
+ function analyticsPlugin(options = {}) {
177
+ const { google, gtm, yandex, bing, facebook, pinterest } = options;
178
+
179
+ return {
180
+ name: 'analytics',
181
+ version: '1.0.0',
182
+ _options: options,
183
+
184
+ /**
185
+ * Public API for other plugins
186
+ */
187
+ api: {
188
+ /**
189
+ * Get configuration
190
+ */
191
+ getConfig() {
192
+ return { ...options };
193
+ },
194
+
195
+ /**
196
+ * Check if a tracker is configured
197
+ */
198
+ hasTracker(name) {
199
+ return !!options[name];
200
+ }
201
+ },
202
+
203
+ /**
204
+ * Register helpers
205
+ */
206
+ register(ctx) {
207
+ // Google Analytics (gtag.js)
208
+ ctx.addHelper('gtag', () => generateGtagScript(google));
209
+
210
+ // Google Tag Manager
211
+ ctx.addHelper('gtm', () => generateGtmScript(gtm));
212
+ ctx.addHelper('gtmNoscript', () => generateGtmNoscript(gtm));
213
+
214
+ // Yandex.Metrika
215
+ ctx.addHelper('yandexMetrika', () => generateYandexScript(yandex));
216
+
217
+ // Microsoft/Bing UET
218
+ ctx.addHelper('bingUET', () => generateBingScript(bing));
219
+
220
+ // Facebook Pixel
221
+ ctx.addHelper('facebookPixel', () => generateFacebookScript(facebook));
222
+
223
+ // Verification meta tags
224
+ ctx.addHelper('verificationTags', () => generateVerificationTags(options));
225
+
226
+ // All analytics scripts combined
227
+ ctx.addHelper('allAnalytics', () => {
228
+ const scripts = [];
229
+
230
+ if (google) scripts.push(generateGtagScript(google));
231
+ if (gtm) scripts.push(generateGtmScript(gtm));
232
+ if (yandex) scripts.push(generateYandexScript(yandex));
233
+ if (bing) scripts.push(generateBingScript(bing));
234
+ if (facebook) scripts.push(generateFacebookScript(facebook));
235
+
236
+ return scripts.filter(Boolean).join('\n\n');
237
+ });
238
+
239
+ // Head scripts (verification + analytics)
240
+ ctx.addHelper('analyticsHead', () => {
241
+ const parts = [];
242
+
243
+ // Verification tags first
244
+ const verificationTags = generateVerificationTags(options);
245
+ if (verificationTags) parts.push(verificationTags);
246
+
247
+ // GTM should be as early as possible
248
+ if (gtm) parts.push(generateGtmScript(gtm));
249
+
250
+ // Other analytics
251
+ if (google) parts.push(generateGtagScript(google));
252
+ if (yandex) parts.push(generateYandexScript(yandex));
253
+ if (bing) parts.push(generateBingScript(bing));
254
+ if (facebook) parts.push(generateFacebookScript(facebook));
255
+
256
+ return parts.filter(Boolean).join('\n\n');
257
+ });
258
+
259
+ // Body open scripts (GTM noscript)
260
+ ctx.addHelper('analyticsBodyOpen', () => {
261
+ return generateGtmNoscript(gtm);
262
+ });
263
+ }
264
+ };
265
+ }
266
+
267
+ module.exports = analyticsPlugin;
268
+
269
+
270
+
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Dashboard Mithril.js Application
3
+ * Components and logic for the dashboard SPA
4
+ */
5
+
6
+ module.exports = `
7
+ // State
8
+ const state = {
9
+ activeTab: 'routes',
10
+ filter: 'all',
11
+ search: '',
12
+ routes: window.__DASHBOARD_DATA__.routes || [],
13
+ plugins: window.__DASHBOARD_DATA__.plugins || [],
14
+ config: window.__DASHBOARD_DATA__.config || {}
15
+ };
16
+
17
+ // Icons
18
+ const Icons = {
19
+ logo: () => m('svg', { viewBox: '0 0 24 24', fill: 'none' }, [
20
+ m('path', { d: 'M12 2L2 7L12 12L22 7L12 2Z', fill: 'currentColor', opacity: '0.3' }),
21
+ m('path', { d: 'M2 17L12 22L22 17', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }),
22
+ m('path', { d: 'M2 12L12 17L22 12', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' })
23
+ ]),
24
+ empty: () => m('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.5' }, [
25
+ m('path', { d: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z' })
26
+ ])
27
+ };
28
+
29
+ // Components
30
+ const Header = {
31
+ view: () => m('.header', [
32
+ m('h1', [
33
+ Icons.logo(),
34
+ 'Webspresso Dashboard'
35
+ ]),
36
+ m('span.dev-badge', 'development')
37
+ ])
38
+ };
39
+
40
+ const Tabs = {
41
+ view: () => m('.tabs', [
42
+ ['routes', 'Routes'],
43
+ ['plugins', 'Plugins'],
44
+ ['config', 'Config']
45
+ ].map(([id, label]) =>
46
+ m('button.tab', {
47
+ class: state.activeTab === id ? 'active' : '',
48
+ onclick: () => { state.activeTab = id; }
49
+ }, label)
50
+ ))
51
+ };
52
+
53
+ const FilterBar = {
54
+ view: () => m('.filter-bar', [
55
+ m('.filter-group', [
56
+ ['all', 'All'],
57
+ ['ssr', 'SSR'],
58
+ ['api', 'API']
59
+ ].map(([id, label]) =>
60
+ m('button.filter-btn', {
61
+ class: state.filter === id ? 'active' : '',
62
+ onclick: () => { state.filter = id; }
63
+ }, label)
64
+ )),
65
+ m('input.search-input', {
66
+ type: 'text',
67
+ placeholder: 'Search routes...',
68
+ value: state.search,
69
+ oninput: (e) => { state.search = e.target.value; }
70
+ })
71
+ ])
72
+ };
73
+
74
+ const MethodBadge = {
75
+ view: (vnode) => m('span.method-badge.method-' + vnode.attrs.method,
76
+ vnode.attrs.method.toUpperCase()
77
+ )
78
+ };
79
+
80
+ const TypeBadge = {
81
+ view: (vnode) => m('span.type-badge.type-' + vnode.attrs.type,
82
+ vnode.attrs.type.toUpperCase()
83
+ )
84
+ };
85
+
86
+ const RoutesTable = {
87
+ view: () => {
88
+ let routes = state.routes;
89
+
90
+ // Apply filter
91
+ if (state.filter !== 'all') {
92
+ routes = routes.filter(r => r.type === state.filter);
93
+ }
94
+
95
+ // Apply search
96
+ if (state.search) {
97
+ const search = state.search.toLowerCase();
98
+ routes = routes.filter(r =>
99
+ r.pattern.toLowerCase().includes(search) ||
100
+ r.file.toLowerCase().includes(search)
101
+ );
102
+ }
103
+
104
+ if (routes.length === 0) {
105
+ return m('.table-container',
106
+ m('.empty-state', [
107
+ Icons.empty(),
108
+ m('p', 'No routes found')
109
+ ])
110
+ );
111
+ }
112
+
113
+ return m('.table-container',
114
+ m('table', [
115
+ m('thead',
116
+ m('tr', [
117
+ m('th', 'Method'),
118
+ m('th', 'Path'),
119
+ m('th', 'File'),
120
+ m('th', 'Type')
121
+ ])
122
+ ),
123
+ m('tbody',
124
+ routes.map(route =>
125
+ m('tr', [
126
+ m('td', m(MethodBadge, { method: route.method })),
127
+ m('td', [
128
+ m('span.code', route.pattern),
129
+ route.isDynamic ? m('span.dynamic-indicator', '⚡ dynamic') : null
130
+ ]),
131
+ m('td', m('span.file-path', route.file)),
132
+ m('td', m(TypeBadge, { type: route.type }))
133
+ ])
134
+ )
135
+ )
136
+ ])
137
+ );
138
+ }
139
+ };
140
+
141
+ const StatsGrid = {
142
+ view: () => {
143
+ const ssrCount = state.routes.filter(r => r.type === 'ssr').length;
144
+ const apiCount = state.routes.filter(r => r.type === 'api').length;
145
+ const dynamicCount = state.routes.filter(r => r.isDynamic).length;
146
+
147
+ return m('.stats-grid', [
148
+ m('.card', [
149
+ m('.card-title', 'Total Routes'),
150
+ m('.card-value', state.routes.length)
151
+ ]),
152
+ m('.card', [
153
+ m('.card-title', 'SSR Pages'),
154
+ m('.card-value', ssrCount)
155
+ ]),
156
+ m('.card', [
157
+ m('.card-title', 'API Endpoints'),
158
+ m('.card-value', apiCount)
159
+ ]),
160
+ m('.card', [
161
+ m('.card-title', 'Dynamic Routes'),
162
+ m('.card-value', dynamicCount)
163
+ ])
164
+ ]);
165
+ }
166
+ };
167
+
168
+ const RoutesView = {
169
+ view: () => m('div', [
170
+ m(StatsGrid),
171
+ m(FilterBar),
172
+ m(RoutesTable)
173
+ ])
174
+ };
175
+
176
+ const PluginsView = {
177
+ view: () => {
178
+ if (state.plugins.length === 0) {
179
+ return m('.table-container',
180
+ m('.empty-state', [
181
+ Icons.empty(),
182
+ m('p', 'No plugins loaded')
183
+ ])
184
+ );
185
+ }
186
+
187
+ return m('.table-container',
188
+ state.plugins.map(plugin =>
189
+ m('.plugin-item', [
190
+ m('div', [
191
+ m('.plugin-name', plugin.name),
192
+ plugin.description ? m('p', { style: 'color: var(--text-secondary); font-size: 13px; margin-top: 4px;' }, plugin.description) : null
193
+ ]),
194
+ m('.plugin-version', 'v' + plugin.version)
195
+ ])
196
+ )
197
+ );
198
+ }
199
+ };
200
+
201
+ const ConfigView = {
202
+ view: () => {
203
+ const config = state.config;
204
+
205
+ return m('div', [
206
+ // Environment
207
+ m('.config-section', [
208
+ m('h3', 'Environment'),
209
+ m('.table-container',
210
+ Object.entries(config.env || {}).map(([key, value]) =>
211
+ m('.config-item', [
212
+ m('.config-key', key),
213
+ m('.config-value', String(value))
214
+ ])
215
+ )
216
+ )
217
+ ]),
218
+
219
+ // i18n
220
+ m('.config-section', [
221
+ m('h3', 'Internationalization'),
222
+ m('.table-container', [
223
+ m('.config-item', [
224
+ m('.config-key', 'Default Locale'),
225
+ m('.config-value', config.i18n?.defaultLocale || 'en')
226
+ ]),
227
+ m('.config-item', [
228
+ m('.config-key', 'Supported Locales'),
229
+ m('.config-value', (config.i18n?.supportedLocales || ['en']).join(', '))
230
+ ])
231
+ ])
232
+ ]),
233
+
234
+ // Server
235
+ m('.config-section', [
236
+ m('h3', 'Server'),
237
+ m('.table-container', [
238
+ m('.config-item', [
239
+ m('.config-key', 'Port'),
240
+ m('.config-value', config.server?.port || '3000')
241
+ ]),
242
+ m('.config-item', [
243
+ m('.config-key', 'Base URL'),
244
+ m('.config-value', config.server?.baseUrl || 'http://localhost:3000')
245
+ ])
246
+ ])
247
+ ])
248
+ ]);
249
+ }
250
+ };
251
+
252
+ const App = {
253
+ view: () => m('.dashboard', [
254
+ m(Header),
255
+ m(Tabs),
256
+ state.activeTab === 'routes' ? m(RoutesView) : null,
257
+ state.activeTab === 'plugins' ? m(PluginsView) : null,
258
+ state.activeTab === 'config' ? m(ConfigView) : null
259
+ ])
260
+ };
261
+
262
+ // Mount app
263
+ m.mount(document.getElementById('app'), App);
264
+ `;
265
+
266
+
267
+
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Webspresso Dashboard Plugin
3
+ * Development dashboard for monitoring routes, plugins, and configuration
4
+ * Uses Mithril.js for the SPA interface
5
+ */
6
+
7
+ const styles = require('./styles');
8
+ const appScript = require('./app');
9
+
10
+ /**
11
+ * Filter sensitive environment variables
12
+ */
13
+ function filterSensitiveEnv(env) {
14
+ const sensitiveKeys = ['SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL', 'PRIVATE'];
15
+ const filtered = {};
16
+
17
+ for (const [key, value] of Object.entries(env)) {
18
+ const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
19
+ filtered[key] = isSensitive ? '••••••••' : value;
20
+ }
21
+
22
+ return filtered;
23
+ }
24
+
25
+ /**
26
+ * Generate the dashboard HTML
27
+ */
28
+ function generateDashboardHtml(data) {
29
+ return `<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <title>Webspresso Dashboard</title>
35
+ <script src="https://unpkg.com/mithril/mithril.js"></script>
36
+ <style>${styles}</style>
37
+ </head>
38
+ <body>
39
+ <div id="app"></div>
40
+ <script>
41
+ window.__DASHBOARD_DATA__ = ${JSON.stringify(data)};
42
+ </script>
43
+ <script>${appScript}</script>
44
+ </body>
45
+ </html>`;
46
+ }
47
+
48
+ /**
49
+ * Create the dashboard plugin
50
+ * @param {Object} options - Plugin options
51
+ * @param {string} options.path - Dashboard path (default: '/_webspresso')
52
+ * @param {boolean} options.enabled - Force enable/disable (default: auto based on NODE_ENV)
53
+ */
54
+ function dashboardPlugin(options = {}) {
55
+ const {
56
+ path: dashboardPath = '/_webspresso',
57
+ enabled
58
+ } = options;
59
+
60
+ // Determine if enabled (default: only in development)
61
+ const isEnabled = enabled !== undefined
62
+ ? enabled
63
+ : process.env.NODE_ENV !== 'production';
64
+
65
+ return {
66
+ name: 'dashboard',
67
+ version: '1.0.0',
68
+ description: 'Development dashboard for monitoring routes and configuration',
69
+
70
+ /**
71
+ * Called after routes are mounted
72
+ */
73
+ onRoutesReady(ctx) {
74
+ // Skip if disabled
75
+ if (!isEnabled) {
76
+ return;
77
+ }
78
+
79
+ const { app, routes, options: appOptions } = ctx;
80
+
81
+ // Collect plugin info
82
+ const pluginManager = ctx.pluginManager || (appOptions && appOptions.pluginManager);
83
+ const plugins = [];
84
+
85
+ // Get plugins from plugin manager if available
86
+ if (pluginManager && pluginManager.plugins) {
87
+ for (const [name, plugin] of pluginManager.plugins) {
88
+ plugins.push({
89
+ name: plugin.name || name,
90
+ version: plugin.version || '0.0.0',
91
+ description: plugin.description || ''
92
+ });
93
+ }
94
+ }
95
+
96
+ // Build config data
97
+ const config = {
98
+ env: filterSensitiveEnv({
99
+ NODE_ENV: process.env.NODE_ENV || 'development',
100
+ PORT: process.env.PORT || '3000',
101
+ BASE_URL: process.env.BASE_URL || 'http://localhost:3000'
102
+ }),
103
+ i18n: {
104
+ defaultLocale: process.env.DEFAULT_LOCALE || 'en',
105
+ supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(',')
106
+ },
107
+ server: {
108
+ port: process.env.PORT || '3000',
109
+ baseUrl: process.env.BASE_URL || 'http://localhost:3000'
110
+ }
111
+ };
112
+
113
+ // Dashboard HTML endpoint
114
+ ctx.addRoute('get', dashboardPath, (req, res) => {
115
+ const data = { routes, plugins, config };
116
+ res.type('text/html');
117
+ res.send(generateDashboardHtml(data));
118
+ });
119
+
120
+ // JSON API endpoints
121
+ ctx.addRoute('get', dashboardPath + '/api/routes', (req, res) => {
122
+ res.json(routes);
123
+ });
124
+
125
+ ctx.addRoute('get', dashboardPath + '/api/plugins', (req, res) => {
126
+ res.json(plugins);
127
+ });
128
+
129
+ ctx.addRoute('get', dashboardPath + '/api/config', (req, res) => {
130
+ res.json(config);
131
+ });
132
+
133
+ // Log dashboard URL
134
+ console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 3000}${dashboardPath}\n`);
135
+ }
136
+ };
137
+ }
138
+
139
+ module.exports = dashboardPlugin;
140
+
141
+
142
+