webspresso 0.0.97 → 0.0.99

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.
Files changed (136) hide show
  1. package/.agents/ADMIN.md +163 -5
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/ARCHITECTURE.md +10 -0
  4. package/.agents/AUTH.md +169 -0
  5. package/.agents/CLI.md +195 -1
  6. package/.agents/ORM.md +88 -4
  7. package/.agents/PLUGINS.md +221 -9
  8. package/.agents/QUEUE.md +127 -0
  9. package/.agents/ROUTING.md +127 -7
  10. package/AGENTS.md +1 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/analytics-prune.js +57 -0
  14. package/bin/commands/api.js +207 -15
  15. package/bin/commands/build.js +99 -0
  16. package/bin/commands/dev.js +64 -35
  17. package/bin/commands/doctor.js +26 -0
  18. package/bin/commands/email-prune.js +47 -0
  19. package/bin/commands/make-job.js +86 -0
  20. package/bin/commands/make-model.js +250 -0
  21. package/bin/commands/new.js +45 -28
  22. package/bin/commands/openapi.js +102 -0
  23. package/bin/commands/page.js +6 -3
  24. package/bin/commands/queue-prune.js +59 -0
  25. package/bin/commands/routes.js +52 -0
  26. package/bin/commands/service.js +6 -3
  27. package/bin/commands/typegen.js +106 -0
  28. package/bin/commands/upload-prune.js +79 -0
  29. package/bin/webspresso.js +22 -0
  30. package/core/applySchema.js +13 -6
  31. package/core/auth/hash.js +21 -12
  32. package/core/auth/index.js +60 -0
  33. package/core/auth/manager.js +134 -0
  34. package/core/auth/middleware.js +88 -0
  35. package/core/auth/oauth.js +517 -0
  36. package/core/auth/qrcode.js +499 -0
  37. package/core/auth/totp.js +504 -0
  38. package/core/compression/middleware.js +23 -17
  39. package/core/openapi/build-from-api-routes.js +160 -29
  40. package/core/orm/cache/fingerprint.js +15 -1
  41. package/core/orm/cache/memory-provider.js +27 -2
  42. package/core/orm/eager-loader.js +144 -10
  43. package/core/orm/index.js +20 -7
  44. package/core/orm/model.js +53 -7
  45. package/core/orm/query-builder.js +90 -4
  46. package/core/orm/repository.js +44 -3
  47. package/core/orm/search.js +213 -0
  48. package/core/queue/adapters/database.js +94 -0
  49. package/core/queue/adapters/memory.js +62 -0
  50. package/core/queue/adapters/redis.js +80 -0
  51. package/core/queue/cron.js +331 -0
  52. package/core/queue/index.js +8 -1
  53. package/core/queue/manager.js +205 -1
  54. package/core/queue/scheduler.js +359 -0
  55. package/core/sse/index.js +19 -0
  56. package/core/sse/stream.js +430 -0
  57. package/core/ssr/stream.js +11 -0
  58. package/index.d.ts +1188 -10
  59. package/index.js +88 -96
  60. package/package.json +1 -1
  61. package/plugins/admin-panel/api.js +279 -17
  62. package/plugins/admin-panel/app.js +114 -22
  63. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  64. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  65. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
  66. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  67. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  68. package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
  69. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  70. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  71. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  72. package/plugins/admin-panel/core/admin-module.js +13 -7
  73. package/plugins/admin-panel/core/api-extensions.js +30 -2
  74. package/plugins/admin-panel/core/registry.js +96 -16
  75. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  76. package/plugins/admin-panel/field-renderers/index.js +2 -0
  77. package/plugins/admin-panel/index.js +96 -0
  78. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  79. package/plugins/admin-panel/modules/admin-users.js +16 -1
  80. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  81. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  82. package/plugins/admin-panel/modules/dashboard.js +283 -0
  83. package/plugins/admin-panel/modules/menu.js +56 -18
  84. package/plugins/admin-panel/modules/user-management.js +24 -6
  85. package/plugins/api-key/crypto.js +101 -0
  86. package/plugins/api-key/index.js +288 -0
  87. package/plugins/api-key/middleware.js +461 -0
  88. package/plugins/api-key/migration-template.js +48 -0
  89. package/plugins/api-key/model.js +50 -0
  90. package/plugins/api-key/scopes.js +125 -0
  91. package/plugins/audit-log/admin-component.js +487 -59
  92. package/plugins/audit-log/api-handlers.js +148 -0
  93. package/plugins/audit-log/index.js +153 -3
  94. package/plugins/audit-log/purge.js +60 -10
  95. package/plugins/dashboard/app.js +152 -27
  96. package/plugins/dashboard/index.js +143 -39
  97. package/plugins/dashboard/styles.js +78 -0
  98. package/plugins/data-exchange/export-xlsx.js +9 -2
  99. package/plugins/data-exchange/import.js +9 -2
  100. package/plugins/data-exchange/index.js +6 -3
  101. package/plugins/email/index.js +132 -2
  102. package/plugins/email/purge.js +60 -10
  103. package/plugins/file-manager/admin-component.js +20 -4
  104. package/plugins/file-manager/index.js +21 -4
  105. package/plugins/index.js +10 -0
  106. package/plugins/oauth/index.js +338 -0
  107. package/plugins/orm-cache-admin/index.js +4 -2
  108. package/plugins/queue/index.js +15 -0
  109. package/plugins/schema-explorer/admin-component.js +714 -0
  110. package/plugins/schema-explorer/relations-graph.js +205 -0
  111. package/plugins/schema-explorer.js +248 -69
  112. package/plugins/site-analytics/admin-component.js +23 -0
  113. package/plugins/site-analytics/api-handlers.js +42 -0
  114. package/plugins/site-analytics/index.js +147 -2
  115. package/plugins/site-analytics/purge.js +168 -0
  116. package/plugins/swagger.js +261 -13
  117. package/plugins/upload/index.js +26 -0
  118. package/plugins/upload/orphan-pruner.js +292 -0
  119. package/src/api/api-loader.js +4 -1
  120. package/src/bundle/index.js +517 -0
  121. package/src/bundle/manifest.js +165 -0
  122. package/src/dev/banner.js +296 -0
  123. package/src/dev/colors.js +70 -0
  124. package/src/dev/index.js +62 -0
  125. package/src/dev/profiler.js +354 -0
  126. package/src/dev/typegen.js +497 -0
  127. package/src/dev-reload/index.js +611 -0
  128. package/src/discovery/file-route-parser.js +44 -1
  129. package/src/discovery/scan-routes.js +2 -2
  130. package/src/file-router.js +347 -162
  131. package/src/helpers.js +967 -98
  132. package/src/njk-frontmatter.js +12 -0
  133. package/src/pages/page-loader.js +61 -11
  134. package/src/routing/cascading.js +378 -0
  135. package/src/routing/middleware-resolver.js +92 -0
  136. package/src/server.js +635 -63
package/.agents/ORM.md CHANGED
@@ -122,17 +122,32 @@ const result = await productRepo.query()
122
122
 
123
123
  ---
124
124
 
125
- ## 3. Relations & Eager Loading
125
+ ## 3. Relations & Nested Eager Loading
126
126
 
127
- Support for `belongsTo`, `hasMany`, and `hasOne`:
127
+ Support for `belongsTo`, `hasMany`, and `hasOne` with arbitrary-depth batch nested loading:
128
128
 
129
129
  ```js
130
- // Eager-load relations using include
131
- const productsWithCategory = await productRepo.query()
130
+ // 1. Direct eager loading
131
+ const products = await productRepo.query()
132
132
  .include('category', 'reviews')
133
133
  .find();
134
+
135
+ // 2. Nested eager loading (dot notation)
136
+ const posts = await postRepo.query()
137
+ .include('author.profile', 'author.company.country', 'comments.author')
138
+ .find();
139
+
140
+ // 3. Eager loading via repository methods
141
+ const post = await postRepo.findById(id, { include: ['author.profile'] });
142
+ const list = await postRepo.find({ active: true }, { include: ['author.company'] });
143
+
144
+ // 4. Shorthand repository chaining
145
+ const result = await postRepo.include('author.profile').where('user_id', 1).find();
134
146
  ```
135
147
 
148
+ > [!TIP]
149
+ > Nested relations are loaded using optimized batch queries (`whereIn`) with zero N+1 queries. Shared object references and JSON columns on related models are automatically handled.
150
+
136
151
  ---
137
152
 
138
153
  ## 4. Ambient Transactions & AsyncLocalStorage
@@ -228,3 +243,72 @@ defineModel({
228
243
  - **Include Guards**: Excessive `query().with(...)` or `?include=...` relations beyond `maxIncludes` throw `QueryComplexityError` or safely cap relations.
229
244
  - **REST Resources Protection**: Automatically validates client parameters (`?perPage=5000` or `?include=...`) against model complexity boundaries.
230
245
 
246
+ ---
247
+
248
+ ## 8. Universal Full-Text Search (FTS)
249
+
250
+ Webspresso ORM provides database-agnostic full-text and keyword search with transparent dialect optimizations for SQLite, PostgreSQL, and MySQL.
251
+
252
+ ### 8.1 Model Configuration (`searchable`)
253
+
254
+ Declare default searchable columns on your model:
255
+
256
+ ```javascript
257
+ defineModel({
258
+ name: 'Article',
259
+ table: 'articles',
260
+ schema: zdb.schema({
261
+ id: zdb.id(),
262
+ title: zdb.string(),
263
+ content: zdb.text(),
264
+ author: zdb.string(),
265
+ }),
266
+ searchable: ['title', 'content'], // Default columns for .search()
267
+ });
268
+ ```
269
+
270
+ *(Note: If `searchable` is not defined, `.search()` automatically falls back to `admin.searchFields` if available.)*
271
+
272
+ ### 8.2 Using Search in Application Code
273
+
274
+ Search is available both on repository instances (`repo.search`) and on query builders (`query().search()` / `query().orSearch()`):
275
+
276
+ ```javascript
277
+ const articleRepo = req.db.getRepository('Article');
278
+
279
+ // 1. Basic search across default searchable columns
280
+ const results = await articleRepo.search('webspresso framework').get();
281
+
282
+ // 2. Chained with filtering, ordering, and pagination
283
+ const page = await articleRepo
284
+ .search(req.query.q) // Safe no-op when req.query.q is empty or undefined
285
+ .where('status', 'published')
286
+ .orderBy('created_at', 'desc')
287
+ .paginate({ page: 1, limit: 10 });
288
+
289
+ // 3. Dynamic column override
290
+ const byAuthor = await articleRepo.search('Alice', ['author']).get();
291
+
292
+ // 4. Exact phrase matching
293
+ const exact = await articleRepo.search('zero dependency sprawl', null, { exact: true }).get();
294
+
295
+ // 5. OR matching between words (default is AND)
296
+ const anyWord = await articleRepo.search('vue react angular', null, { matchAllWords: false }).get();
297
+
298
+ // 6. Disjoined search conditions with orSearch()
299
+ const mixed = await articleRepo.query()
300
+ .where('author', 'Bob')
301
+ .orSearch('featured article', ['title'])
302
+ .get();
303
+ ```
304
+
305
+ ### 8.3 Dialect Features & Modes
306
+
307
+ | Dialect | Mode (`options.mode`) | SQL Strategy | Notes |
308
+ | :--- | :--- | :--- | :--- |
309
+ | **SQLite** | `'auto'` (default) | Tokenized case-insensitive `LIKE` | Safe out-of-the-box, no virtual table required. |
310
+ | **SQLite** | `'fts'` | `articles MATCH ?` or virtual table subquery | Set `{ mode: 'fts', ftsTable: 'articles_fts' }` for FTS5 tables. |
311
+ | **PostgreSQL** | `'auto'` / `'fts'` | `to_tsvector @@ plainto_tsquery` / `websearch_to_tsquery` | Native Postgres tsvector search. Pass `{ rank: true }` to order by `ts_rank` relevance. |
312
+ | **MySQL** | `'fts'` | `MATCH(...) AGAINST(? IN BOOLEAN MODE)` | Requires FULLTEXT index on table. |
313
+
314
+
@@ -77,6 +77,41 @@ Zero-dependency HTTP Basic Authentication (RFC 7617) plugin with timing-safe equ
77
77
  - `onAuthenticated`: Callback hook `(req, user) => ...`.
78
78
  - **Named Middleware**: Registers `ctx.middlewares.basicAuth` for route files.
79
79
 
80
+ ### 2.2 `apiKeyPlugin` (`plugins/api-key`)
81
+ Zero-dependency B2B & Developer API Key Management Plugin with standard prefix format (`wsp_live_...`), SHA-256 secure hash storage, timing-safe constant-time verification (`crypto.timingSafeEqual`), granular scopes/permissions with wildcard support, non-blocking `last_used_at` usage tracking, and programmatic creation/revocation APIs.
82
+ - **Options**:
83
+ - `db` / `knex`: Webspresso Database or Knex instance.
84
+ - `tableName`: Database table name (default `'api_keys'`).
85
+ - `prefix`: Default key prefix (default `'wsp_live_'`).
86
+ - `byteLength`: Random byte count for key generation (default `24`).
87
+ - `global`: Boolean, whether to protect routes globally (default `false`).
88
+ - `routes`: Route prefix(es) to protect when `global: true`.
89
+ - `skipPaths` / `skip`: Exclusion paths or predicate function `(req) => boolean`.
90
+ - `keys`: Static in-memory keys for testing / serverless / simple setups (`[{ key, name, scopes }]` or map).
91
+ - `trackUsage`: Boolean (default `true`), whether to automatically update `last_used_at` on verified keys.
92
+ - `updateLastUsedThrottleMs`: Throttle period in ms to avoid DB write thrashing (default `60000`).
93
+ - `allowQueryParam`: Boolean (default `false`), allow extracting API key via `?apiKey=...`.
94
+ - `findKeyByHash` / `verify`: Custom key lookup or verification hook.
95
+ - `getUser`: Custom async user resolver `(apiKeyRecord, req) => Promise<User | null>`.
96
+ - `onAuthenticated`: Callback hook `(req, apiKeyRecord) => void`.
97
+ - **Named Middleware**:
98
+ - `middleware: ['apiKey']`: Requires valid API key via `X-API-Key`, `api-key`, or `Authorization: Bearer <key>`.
99
+ - `middleware: [['apiKey', { scopes: ['reports:read', 'orders:*'] }]]`: Enforces required granular scopes with prefix wildcard support (`*`, `namespace:*`).
100
+ - `middleware: [['apiKey', { optional: true }]]`: Non-blocking authentication (populates `req.apiKey` if present, passes through if absent).
101
+ - **Request Context Injected**:
102
+ - `req.apiKey`: The authenticated API key record (including name, key_hint, scopes, metadata).
103
+ - `req.auth.apiKey`: Equivalent reference inside `req.auth`.
104
+ - `req.auth.user` & `req.user`: Associated user record (if `user_id` or `getUser` resolved).
105
+ - **Programmatic API (`apiKeyPlugin.api`)**:
106
+ - `generateKey(options)`: Generate fresh key with hash and masked hint (`wsp_live_...4f2a`).
107
+ - `hashKey(rawKey)`: Compute SHA-256 hex string.
108
+ - `maskKey(rawKey, prefix)`: Mask raw key for logs/UI.
109
+ - `createApiKey(data, options)`: Generates key, hashes it, and stores in database table (returns raw key once).
110
+ - `verifyApiKey(rawKey, options)`: Verify raw key against DB, checking expiration and revocation.
111
+ - `revokeApiKey(idOrHash, options)`: Mark key as revoked in DB (`revoked_at = now()`).
112
+ - `getMigrationTemplate(tableName)`: Knex migration script creating `api_keys` table.
113
+ - `createApiKeyModel(tableName)`: Webspresso ORM model definition with hidden `key_hash`.
114
+
80
115
  ### 2.2 `adminPanelPlugin` (`plugins/admin-panel`)
81
116
  Mithril.js SPA Admin Panel mounted at `/_admin`.
82
117
  - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
@@ -91,6 +126,8 @@ Schema-driven headless CMS & inline content editing.
91
126
  Multipart file upload manager with local / cloud storage providers.
92
127
  - **Options**: `{ path: '/api/upload', local: { destDir, publicBasePath }, maxBytes: 10000000, mimeAllowlist: ['image/png', 'image/jpeg'] }`
93
128
  - **Features**: Single & multi-file uploads, file size & extension validation, storage drivers.
129
+ - **Orphan Pruning**: `scanOrphanedUploads({ db, destDir })` and `pruneOrphanedUploads({ db, destDir, dryRun })` to detect and remove disk files no longer referenced by any database model, accompanied by CLI command `webspresso upload:prune [--dry-run]`.
130
+
94
131
 
95
132
  ### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
96
133
  Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
@@ -122,12 +159,49 @@ Request rate limiting using express-rate-limit logic.
122
159
  - **Options**: `{ windowMs: 15 * 60 * 1000, max: 100 }`
123
160
 
124
161
  ### 2.9 `siteAnalyticsPlugin` (`plugins/site-analytics`)
125
- Self-hosted privacy-focused page view analytics and client error tracking.
126
- - **Options**: `{ path: '/_analytics', db }`
162
+ Self-hosted privacy-focused page view analytics, client error tracking, and automated retention cleanup.
163
+ - **Options**:
164
+ - `db`: Knex database instance.
165
+ - `tableName`: Table name for page views (default: `'analytics_page_views'`).
166
+ - `errorsTableName`: Table name for client errors (default: `'analytics_client_errors'`).
167
+ - `retentionDays`: Page views retention window in days (default: `90`).
168
+ - `errorRetentionDays`: Client error logs retention window in days (default: `30`).
169
+ - `cleanupIntervalMs`: Background scheduled cleanup interval in milliseconds (default: `86400000` / 24 hours).
170
+ - `autoCleanup`: Enable automated periodic background cleanup (default: `true`).
171
+ - `cleanupOnStart`: Run an initial non-blocking cleanup shortly after startup (default: `true`).
172
+ - `retention`: Shorthand object `{ enabled, days, errorDays, intervalMs, runOnStart }` or `false` to disable retention cleanup.
173
+ - `excludePaths`, `excludeBots`, `customBots`, `anonymizeIp`, `trustProxy`, `collectErrors`, `dashboard`.
174
+ - **Programmatic API (`plugin.api`)**:
175
+ - `purge({ days, errorDays, olderThan, errorsOlderThan })`: Manual purge of page views and error logs.
176
+ - `purgeAnalyticsData(knex, options)`: Standalone query purge helper.
177
+ - `startCleanup(intervalMs)` / `stopCleanup()`: Control background cleanup lifecycle.
178
+ - `getRetentionConfig()`: Inspect active retention configuration.
179
+ - **Admin REST Endpoint**: `POST /_admin/api/analytics/purge` (accepts `{ days, errorDays }`).
180
+ - **Admin UI**: "Purge Old Data" button with confirmation prompt in the Analytics admin dashboard header.
181
+ - **CLI Command**:
182
+ ```bash
183
+ webspresso analytics:prune --days 90 --error-days 30
184
+ ```
185
+ - **Lifecycle Integration**: Background timer uses `.unref()` to avoid keeping Node event loop open, and registers a framework disposer to clear timers on shutdown.
127
186
 
128
187
  ### 2.10 `emailPlugin` (`plugins/email`)
129
- Transactional email compiler (MJML) & Nodemailer transport integration.
130
- - **Options**: `{ transport, from: 'noreply@example.com', templatesDir }`
188
+ Transactional email compiler (MJML) & Nodemailer transport integration with database delivery logging and auto-retention cleanup.
189
+ - **Options**:
190
+ - `transport` / `smtp`: Nodemailer transport or SMTP settings.
191
+ - `defaults`: Default sender/reply-to address.
192
+ - `templatesDir`: Directory containing `.mjml` templates.
193
+ - `logToDb`: Store delivery history in database (default `true` when `db` is supplied).
194
+ - `tableName`: Database table name (default `'email_logs'`).
195
+ - `retentionDays`: Days to retain email logs (default `90`).
196
+ - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
197
+ - `autoCleanup`: Enable scheduled background purge (default `true`).
198
+ - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
199
+ - `retention`: `{ enabled, days: 90, intervalMs: 86400000, runOnStart: true }` (or `false` to disable).
200
+ - **Pruning CLI**:
201
+ ```bash
202
+ webspresso email:prune --days 90
203
+ ```
204
+ - **Admin Panel UI**: Includes log listing, test email dispatch, template preview, connection verification, and "Purge 90d" action.
131
205
 
132
206
  ### 2.11 `polarPlugin` (`plugins/polar`)
133
207
  Polar.sh subscription billing — checkout, customer portal, webhooks, Customer State sync, tier columns. Zero extra deps (`crypto` + `fetch`).
@@ -178,17 +252,100 @@ Polar.sh subscription billing — checkout, customer portal, webhooks, Customer
178
252
  - [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md)
179
253
 
180
254
  ### 2.12 `auditLogPlugin` (`plugins/audit-log`)
181
- Admin mutation tracking and audit log audit history.
182
- - **Options**: `{ db, retainDays: 90 }`
255
+ Admin mutation tracking, record change timeline, and audit history with scheduled retention and manual purge API.
256
+ - **Options**:
257
+ - `db`: Database instance (must expose `.knex`).
258
+ - `tableName`: Database table name (default `'audit_logs'`).
259
+ - `registerRecordAction`: Register built-in "Zaman Tüneli" action on all admin models (default `true`).
260
+ - `retentionDays`: Days to retain audit trail entries (default `90`).
261
+ - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
262
+ - `autoCleanup`: Enable scheduled background purge (default `true`).
263
+ - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
264
+ - `retention`: `{ enabled, days: 90, intervalMs: 86400000, runOnStart: true }` (or `false` to disable).
265
+ - **Zaman Tüneli (Timeline UI) Features**:
266
+ - **Dual Mode**: Switch seamlessly between **Zaman Tüneli (Timeline View)** and **Tablo Görünümü (Table View)**.
267
+ - **Record Focus Mode**: Clicking any `Model #ID` tag instantly filters and displays that specific record's revision timeline.
268
+ - **Visual Spine & Status Icons**: Color-coded action nodes for `create` (emerald +), `update` (blue ✏️), `delete` (rose 🗑️), and `restore` (amber ↺).
269
+ - **Changed Fields Diff**: Clearly lists modified fields (e.g. `title`, `price`) on every update event.
270
+ - **Expandable Details**: Inspect full HTTP request path, actor IP, user agent, and pretty-printed JSON metadata.
271
+ - **Built-in Record Action**: One-click "Zaman Tüneli" button on record lists and edit forms jumps straight to that record's audit history.
272
+ - **API & Admin Panel Endpoints**:
273
+ - `GET /_admin/api/audit-logs`: List paginated logs with filters (`model`, `recordId`, `action`, `search`, `from`, `to`).
274
+ - `GET /_admin/api/audit-logs/timeline/:model/:id`: Dedicated chronological record change timeline with summary stats.
275
+ - `POST /_admin/api/audit-logs/purge`: `{ days: 90 }` or `{ olderThan: '2026-01-01' }`.
276
+ - **Programmatic Plugin API**:
277
+ ```javascript
278
+ const audit = ctx.usePlugin('audit-log');
279
+ const history = await audit.queryTimeline('Post', 42);
280
+ const logs = await audit.queryLogs({ model: 'User', action: 'delete' });
281
+ ```
282
+ - **Pruning CLI**:
283
+ ```bash
284
+ webspresso audit:prune --days 90
285
+ ```
183
286
 
184
287
  ### 2.13 `ormCacheAdminPlugin` (`plugins/orm-cache-admin`)
185
288
  ORM Cache inspection and invalidation dashboard.
186
289
 
187
290
  ### 2.14 `schemaExplorerPlugin` (`plugins/schema-explorer`)
188
- Interactive JSON schema & OpenAPI route specification generator.
291
+ Interactive Database Schema & ER Diagram Explorer for both raw JSON/OpenAPI endpoints and Admin Panel SPA.
292
+ - **Admin Panel Page (`/_admin/schema`)**:
293
+ - Interactive Table & Column details (Primary Keys, Foreign Keys, Nullability, Types, Constraints, Scopes).
294
+ - Visual Relationship Map (ER Diagram) with bidirectional incoming & outgoing relation links.
295
+ - Interactive table cards highlighting connected models and key mappings (`posts.author_id ➔ authors.id`).
296
+ - One-click Mermaid.js ER diagram code generation and copy.
297
+ - JSON schema export.
298
+ - **Options**:
299
+ - `admin: true` — Automatically registers into `adminPanelPlugin` when present.
300
+ - `adminPath: '/schema'` — Custom path inside Admin Panel.
301
+ - `menuLabel: 'Schema Explorer'` — Sidebar menu label.
302
+ - `menuIcon: 'database'` — Sidebar icon.
303
+ - `path: '/_schema'` — Raw JSON endpoint path.
304
+ - `exclude: ['SecretModel']` — Models to exclude.
305
+ - **Usage**:
306
+ ```js
307
+ const { createApp } = require('webspresso');
308
+ const { adminPanelPlugin, schemaExplorerPlugin } = require('webspresso/plugins');
189
309
 
190
- ### 2.15 `swaggerPlugin` (`plugins/swagger`)
191
- Interactive OpenAPI 3.0 Swagger UI documentation.
310
+ const { app } = createApp({
311
+ plugins: [
312
+ adminPanelPlugin({ db }),
313
+ schemaExplorerPlugin({ admin: true }),
314
+ ],
315
+ });
316
+ ```
317
+
318
+ ### 2.15 `swaggerPlugin` / `openapiPlugin` (`plugins/swagger`)
319
+ Interactive OpenAPI 3.0 API documentation supporting both modern **Scalar API Reference** and classic **Swagger UI**, automated parameter/tag/summary inference, auth detection, Admin Panel embedding, and CLI export.
320
+
321
+ - **Options**:
322
+ - `path`: Base path for documentation (default: `/_swagger`). Spec served at `{path}/openapi.json`.
323
+ - `ui`: Default playground engine: `'swagger'` or `'scalar'` (default: `'swagger'`).
324
+ - `theme`: Theme for Scalar UI (`'default'`, `'purple'`, `'moon'`, `'solarized'`, etc.).
325
+ - `darkMode`: Scalar dark mode toggle (default: `true`).
326
+ - `admin`: Auto-register inside Admin Panel (`/_admin/#!/api-docs`) when `admin-panel` plugin is loaded (default: `true`).
327
+ - `adminTitle`: Admin navigation label (default: `'API Docs'`).
328
+ - `includeOrmSchemas`: Merge ORM models into `components.schemas` (default: `false`).
329
+ - `authorize`: Custom `(req) => boolean` guard function.
330
+ - **Endpoints**:
331
+ - `GET /_swagger`: Primary UI (Scalar or Swagger based on `ui` option).
332
+ - `GET /_swagger/scalar`: Always serves modern Scalar UI.
333
+ - `GET /_swagger/swagger`: Always serves Swagger UI.
334
+ - `GET /_swagger/openapi.json`: OpenAPI 3.0 specification document.
335
+ - **Example**:
336
+ ```javascript
337
+ const { createApp, openapiPlugin } = require('webspresso');
338
+
339
+ const { app } = createApp({
340
+ plugins: [
341
+ openapiPlugin({
342
+ ui: 'scalar',
343
+ theme: 'purple',
344
+ includeOrmSchemas: true,
345
+ }),
346
+ ],
347
+ });
348
+ ```
192
349
 
193
350
  ### 2.16 `realtimePlugin` (`plugins/realtime`, `core/realtime`)
194
351
  Framework-agnostic, plugin-based, and adapter-driven Realtime layer supporting WebSocket, SSE, Socket.IO, and distributed Redis Pub/Sub transports with zero SSR execution hazards.
@@ -415,5 +572,60 @@ RFC 4180 compliant CSV generator, parser, ORM model export, streaming chunk writ
415
572
  - `csv.createStreamWriter(stream, options)` → `{ writeHeader, writeRow, writeRows, end }`
416
573
  - **Built-in Services**: `csv.generate`, `csv.parse`, `csv.exportModel` accessible via `ctx.service()` or `req.service()`.
417
574
 
575
+ ### 2.19 `dashboardPlugin` / `devToolbarPlugin` (`plugins/dashboard`)
576
+ Interactive Webspresso Developer Dashboard SPA mounted at `/_webspresso` and Floating In-Page DevTools bar (`fsy.devToolbar()`) inspired by Django Debug Toolbar and Nuxt DevTools.
577
+
578
+ - **Aliases**: `dashboardPlugin`, `devToolbarPlugin`, `devtoolsPlugin`.
579
+ - **Options**:
580
+ - `path`: Base URL path for dashboard (default `/_webspresso`).
581
+ - `title`: Dashboard browser title (default `'Webspresso Developer Dashboard'`).
582
+ - `enabled`: Boolean to force enable in production (default `process.env.NODE_ENV !== 'production'`).
583
+ - **Dashboard Features (`/_webspresso`)**:
584
+ - **Routes Tab**: Full table of file-based SSR and API routes with live search, method badges, dynamic parameters (`[id]`), and target files.
585
+ - **Models Tab**: All registered ORM models with database tables, columns list, cache strategy (`smart` / `auto`), and soft-delete status.
586
+ - **Services Tab**: All business services with transaction safety status, authentication guards (`admin`, roles), schemas, and cache TTL.
587
+ - **Plugins & Config Tabs**: Active plugin manifests, dependencies, and sanitized framework configuration.
588
+ - **JSON API Endpoints**:
589
+ - `GET /_webspresso/api/routes`
590
+ - `GET /_webspresso/api/plugins`
591
+ - `GET /_webspresso/api/config`
592
+ - `GET /_webspresso/api/models`
593
+ - `GET /_webspresso/api/services`
594
+ - `GET /_webspresso/api/overview` (aggregate system metrics, memory, uptime, node version)
595
+ - **Floating In-Page Dev Toolbar (`fsy.devToolbar()`)**:
596
+ - Automatically embedded in `views/layout.njk` during development (`NODE_ENV !== 'production'`).
597
+ - **Collapsed Bottom Pill**:
598
+ - `⚡ DEV` toggle button with `Alt+D` hotkey shortcut.
599
+ - Current route method, HTTP status code (`[GET 200] /path`).
600
+ - Ambient database queries pill (`🗄️ N queries (Xms)`) from `AsyncLocalStorage` request profiler.
601
+ - Prominent N+1 warning alert badge (`⚠️ N+1 (M)`).
602
+ - Quick access buttons to `Dashboard (/_webspresso)`, `Admin (/_admin)`, `API Docs (/_swagger)`, and `Schema (/_schema)`.
603
+ - **Expandable DevTools Drawer (toggled via click or `Alt+D`)**:
604
+ - **🗄️ SQL Queries Tab**: Execution duration (<40ms green, >40ms red), table pills, formatted SQL statement, and N+1 warning box with actionable suggestions (`repo.query().include(...)`).
605
+ - **📄 Route & Page Tab**: Route path, matched file, template name, params, and parsed query string.
606
+ - **🚀 Quick Links Tab**: Configurable tool cards for fast navigation between admin, swagger, and custom tooling.
607
+ - **💻 System Tab**: Process uptime, memory usage, environment, and Node platform metrics.
608
+ - **Zero Production Overhead**: When `process.env.NODE_ENV === 'production'`, `devToolbar()` immediately returns `""` with zero overhead.
609
+
610
+ ### 2.19 `oauthPlugin` (`plugins/oauth`)
611
+ Zero-dependency OAuth 2.0 & Social Login plugin supporting Google, GitHub, and custom providers with RFC 7636 PKCE, HMAC-signed anti-CSRF state tokens, and Dual Auth integration.
612
+ - **Options**:
613
+ - `secret`: Secret key for HMAC state signing (`process.env.OAUTH_SECRET || process.env.SESSION_SECRET`).
614
+ - `pathPrefix`: Base path for OAuth routes (default `'/auth'`).
615
+ - `providers`: Provider configuration dictionary (`google`, `github`, or custom).
616
+ - `defaultReturnTo`: Fallback redirect path after successful login (default `'/'`).
617
+ - `failureRedirect`: Redirect path on OAuth failure (default `'/login'`).
618
+ - `findOrCreateUser`: `async ({ provider, profile, tokens, db, req, res }) => user`.
619
+ - `onSuccess`: `async ({ provider, user, profile, tokens, stateData, req, res, client }) => void`.
620
+ - `onError`: `async ({ error, provider, req, res, client }) => void`.
621
+ - **Automatic Routes**:
622
+ - `GET /auth/:provider`: Initiates OAuth authorization flow with PKCE and signed state.
623
+ - `GET /auth/:provider/callback`: Exchanges authorization code, fetches normalized profile, links or creates user, and logs into session or returns JWT token.
624
+ - **Template Helper**:
625
+ - `fsy.oauthUrl('google')` ➔ `"/auth/google"`
626
+ - `fsy.oauthUrl('github', { returnTo: '/dashboard' })` ➔ `"/auth/github?returnTo=%2Fdashboard"`
627
+
628
+
629
+
418
630
 
419
631
 
package/.agents/QUEUE.md CHANGED
@@ -111,3 +111,130 @@ module.exports.options = {
111
111
  backoff: { type: 'exponential', delay: 2000 },
112
112
  };
113
113
  ```
114
+
115
+ ---
116
+
117
+ ## 5. Job Retention & Pruning
118
+
119
+ To prevent the queue database table or memory cache from accumulating indefinitely:
120
+
121
+ ### 1. Auto-Retention in `createQueueManager` / `queuePlugin`
122
+ ```js
123
+ queuePlugin({
124
+ adapter: 'database',
125
+ db: appDb,
126
+ retentionDays: 7, // Keep completed jobs for 7 days
127
+ cleanupIntervalMs: 24 * 60 * 60 * 1000, // Run every 24h
128
+ autoCleanup: true,
129
+ retentionStatuses: ['completed'], // or ['completed', 'failed']
130
+ });
131
+ ```
132
+
133
+ ### 2. Programmatic `clean()`
134
+ ```js
135
+ // Delete completed jobs older than 7 days
136
+ const deletedCount = await queue.clean({
137
+ days: 7,
138
+ status: ['completed'], // or 'failed', or ['completed', 'failed']
139
+ });
140
+
141
+ // Or with an explicit cutoff date:
142
+ await queue.clean({
143
+ olderThan: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
144
+ status: 'failed',
145
+ });
146
+ ```
147
+
148
+ ### 3. CLI Command: `queue:prune`
149
+ ```bash
150
+ # Delete completed jobs older than 7 days
151
+ webspresso queue:prune --days 7
152
+
153
+ # Delete both completed and failed jobs older than 14 days
154
+ webspresso queue:prune --days 14 --status completed,failed
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 6. Cron & Recurring Task Scheduler
160
+
161
+ Webspresso includes a native, zero-dependency task scheduler with a POSIX-compliant 5-field cron parser (`core/queue/cron.js` & `core/queue/scheduler.js`).
162
+
163
+ ### 1. Cron Expression Syntax
164
+ - Standard 5 fields: `minute hour day-of-month month day-of-week`
165
+ - Wildcards: `*`
166
+ - Steps: `*/5`, `10-30/5`
167
+ - Ranges: `1-5`, `MON-FRI`, `JAN-DEC`
168
+ - Lists: `1,15,30`, `MON,WED,FRI`
169
+ - Descriptive aliases:
170
+ - `@yearly`, `@annually` (`0 0 1 1 *`)
171
+ - `@monthly` (`0 0 1 * *`)
172
+ - `@weekly` (`0 0 * * 0`)
173
+ - `@daily`, `@midnight` (`0 0 * * *`)
174
+ - `@hourly` (`0 * * * *`)
175
+ - `@every_minute`, `@minutely` (`* * * * *`)
176
+ - `@every <N>m` (e.g. `@every 5m` → `*/5 * * * *`)
177
+ - `@every <N>h` (e.g. `@every 2h` → `0 */2 * * *`)
178
+
179
+ ### 2. Programmatic Scheduling
180
+ ```js
181
+ // Schedule daily digest at 09:00 AM
182
+ queue.schedule('0 9 * * *', 'reports.daily-digest', { channel: 'slack' }, {
183
+ overlap: false, // Prevent concurrent execution if previous run is still active (default: false)
184
+ maxRuns: 10, // Optional maximum executions before auto-unscheduling
185
+ utc: false, // Use local timezone or UTC
186
+ });
187
+
188
+ // Or use the .cron() alias
189
+ queue.cron('@every 15m', 'metrics.collect');
190
+
191
+ // Query schedules
192
+ const schedules = queue.getSchedules();
193
+ // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, runsCount, ... }]
194
+
195
+ // Unschedule by schedule ID or job name
196
+ queue.unschedule('metrics.collect');
197
+ ```
198
+
199
+ ### 3. Schedule Options
200
+ | Option | Type | Default | Description |
201
+ | :--- | :--- | :--- | :--- |
202
+ | `id` | `string` | `schedule:${job}:${cron}` | Custom unique identifier |
203
+ | `overlap` | `boolean` | `false` | When `false`, skips tick if a previous job run is still processing |
204
+ | `maxRuns` | `number` | `null` | Automatically unschedules task after N runs |
205
+ | `utc` | `boolean` | `false` | Evaluate cron expressions in UTC instead of local timezone |
206
+ | `runImmediately` | `boolean` | `false` | Triggers a run right after scheduling |
207
+
208
+ ### 4. File-Based Cron Discovery (`jobs/`)
209
+ Jobs in the `jobs/` folder can automatically declare their recurrence in two ways:
210
+
211
+ #### Option A: `cron` / `schedule` property in job module
212
+ ```js
213
+ // jobs/reports/summary.js
214
+ module.exports = async function(job, ctx) {
215
+ // run summary logic...
216
+ };
217
+
218
+ module.exports.cron = '0 8 * * 1-5'; // Every weekday at 8:00 AM
219
+ module.exports.data = { reportType: 'executive' };
220
+ module.exports.options = {
221
+ overlap: false,
222
+ };
223
+ ```
224
+
225
+ #### Option B: `.cron.js` file naming
226
+ ```js
227
+ // jobs/cleanup-logs.cron.js (auto-discovers job name 'cleanup-logs')
228
+ module.exports = {
229
+ cron: '@daily',
230
+ handler: async function(job, ctx) {
231
+ // perform cleanup
232
+ },
233
+ };
234
+ ```
235
+
236
+ ### 5. Lifecycle & Overlap Management
237
+ - The scheduler automatically pauses when `queue.pause()` is called and resumes with `queue.resume()`.
238
+ - On `queue.drain()` or `queue.stop()`, recurring schedules are stopped and active dispatched jobs are given time to complete.
239
+ - Overlap prevention (`overlap: false`) hooks into `job:completed` and `job:failed` events, ensuring slow asynchronous tasks never collide or duplicate.
240
+