webspresso 0.0.99 → 0.0.101

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 (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
package/.agents/ERRORS.md CHANGED
@@ -199,3 +199,62 @@ In server logs, 500 errors are automatically highlighted with visual banners pin
199
199
  Phase: handler
200
200
  ```
201
201
 
202
+ ---
203
+
204
+ ## 6. Directory-Level Error Boundaries (`_error.njk` & `_error.js`)
205
+
206
+ In addition to API JSON errors and the top-level 500 error page, Webspresso provides **Directory-Level Error Boundaries** for SSR pages (similar to Next.js `error.js` or Nuxt `error.vue`):
207
+
208
+ ```
209
+ pages/
210
+ ├── _layout.njk # Root site layout
211
+ ├── _error.njk # Global site error fallback
212
+ └── dashboard/
213
+ ├── _layout.njk # Dashboard layout (sidebar, header)
214
+ ├── _error.njk # Dashboard-scoped error boundary
215
+ ├── _error.js # Optional companion loader
216
+ ├── billing/
217
+ │ └── invoices.njk # If load() fails here...
218
+ └── settings.njk
219
+ ```
220
+
221
+ ### 6.1 Isolated Error Containment
222
+ When an unhandled error is thrown in a page route's `load()`, template rendering, or lifecycle hooks:
223
+ 1. Webspresso searches up the directory hierarchy (innermost leaf to root `pages/`) for the nearest `_error.njk`.
224
+ 2. The error boundary is rendered **inside** the ancestor cascading layouts (`_layout.njk`), ensuring that headers, sidebars, and navigation remain visible while displaying the isolated error state.
225
+ 3. The HTTP response status is preserved from `error.status || error.statusCode || 500` (e.g. throwing `NotFoundError` sets HTTP 404).
226
+
227
+ ### 6.2 Companion Loader (`_error.js`)
228
+ An optional companion `_error.js` can prefetch support data, report errors to monitoring services, or enrich the context:
229
+
230
+ ```javascript
231
+ // pages/dashboard/_error.js
232
+ module.exports = {
233
+ load: async ({ error, req, res, ctx, db }) => {
234
+ return {
235
+ supportEmail: 'support@example.com',
236
+ incidentId: req.id,
237
+ };
238
+ },
239
+ };
240
+ ```
241
+
242
+ ### 6.3 Template Context Variables
243
+ Inside `_error.njk`, the following variables are injected:
244
+ - `error`: `{ message, status, statusCode, code, name, details, fields, stack }` (stack is only exposed when `isDev === true`).
245
+ - `route`: `{ routePath, file }`.
246
+ - `req`: `{ path, query, params, method, url }`.
247
+ - `fsy`: Template helper suite.
248
+ - Plus any data returned by companion `_error.js` loader.
249
+
250
+ ### 6.4 Layout Opt-Out
251
+ To render a full-page custom error view without the parent layout, set `layout: false` in `_error.js` or frontmatter:
252
+ ```yaml
253
+ ---
254
+ layout: false
255
+ ---
256
+ <div class="standalone-error-screen">
257
+ <h1>{{ error.status }} - {{ error.message }}</h1>
258
+ </div>
259
+ ```
260
+
@@ -0,0 +1,179 @@
1
+ # Webspresso Events & Signals Architecture
2
+
3
+ [← Back to AGENTS.md](.agents/AGENTS.md)
4
+
5
+ Webspresso provides an enterprise, zero-dependency **Decoupled Event & Signals System** (`core/events`), combining **AdonisJS Events** (wildcard matching, async parallel/serial/waterfall dispatch, queue bridging, and listener auto-discovery) with **Django Signals** (declarative `Signal` channels, sender filtering, and ORM model signals).
6
+
7
+ ---
8
+
9
+ ## 1. Overview & Key Capabilities
10
+
11
+ - **Zero External Dependencies**: Built entirely with native Node.js standard modules (`events`, `path`, `fs`).
12
+ - **Synchronous & Asynchronous Emission**:
13
+ - `events.emit(name, data)`: Fast sync/fire-and-forget dispatch.
14
+ - `await events.emitAsync(name, data)`: Parallel concurrent execution (`Promise.allSettled`).
15
+ - `await events.emitSerial(name, data)`: Sequential execution in priority order.
16
+ - `await events.emitWaterfall(name, initialData)`: Sequential pipeline where output of listener $N$ feeds listener $N+1$.
17
+ - **Wildcard & Pattern Matching**:
18
+ - `events.on('order.*', (data, meta) => ...)`
19
+ - `events.on('system.**', (data, meta) => ...)`
20
+ - `events.onAny((eventName, data, meta) => ...)`
21
+ - **Django-Style Signals & Sender Filtering**:
22
+ - `const orderPlaced = defineSignal('order_placed')`
23
+ - `orderPlaced.connect(receiver, { sender: 'Checkout' })`
24
+ - `orderPlaced.send({ orderId: 101 }, { sender: 'Checkout' })`
25
+ - **Background Queue Bridging**:
26
+ - `events.listenAndQueue('user.created', 'send-welcome-email', { delay: 1000 })`
27
+ - Automatically bridges emitted events directly to Webspresso's job queue (`core/queue`).
28
+ - **ORM Model Signals Auto-Bridge**:
29
+ - Automatically translates `ModelEvents` (`User.afterCreate`, `*.afterDelete`) to application events (`model:created`, `user:created`, `model:deleted`) with `sender: ctx.model`.
30
+ - **Automatic Request & Context Decoration**:
31
+ - Injected onto `req.events` on every Express request.
32
+ - Available inside `req.context.events` for services: `ctx.events.emit('order.created', order)`.
33
+ - Accessible on `app.events` and plugin contexts.
34
+ - **Graceful Shutdown Integration**:
35
+ - Cleaned up automatically on graceful shutdown via `ShutdownManager.onShutdown()`.
36
+
37
+ ---
38
+
39
+ ## 2. Using Events in API Routes & Services
40
+
41
+ ### 2.1 Emitting Events in File-Based Routes (`pages/api/...`)
42
+
43
+ ```javascript
44
+ // pages/api/orders.post.js
45
+ const { defineApi } = require('webspresso');
46
+
47
+ module.exports = defineApi({
48
+ schema: ({ z }) => ({
49
+ body: z.object({
50
+ total: z.number().positive(),
51
+ items: z.array(z.string()),
52
+ }),
53
+ }),
54
+ async handler(req, res) {
55
+ const order = await req.db.getRepository('Order').create(req.input.body);
56
+
57
+ // Synchronous fire-and-forget or awaited async parallel
58
+ req.events.emit('order.created', order, { sender: 'Order' });
59
+ // or: await req.events.emitAsync('order.created', order);
60
+
61
+ return res.status(201).json(order);
62
+ },
63
+ });
64
+ ```
65
+
66
+ ### 2.2 Emitting Events inside Services (`services/...`)
67
+
68
+ ```javascript
69
+ // services/order/checkout.js
70
+ const { defineService } = require('webspresso');
71
+
72
+ module.exports = defineService({
73
+ transaction: true,
74
+ async handle(input, ctx) {
75
+ const order = await ctx.db.getRepository('Order').create(input);
76
+
77
+ // ctx.events is automatically available
78
+ ctx.events.emit('order.checkout.completed', { orderId: order.id, user: ctx.req?.user });
79
+
80
+ return order;
81
+ },
82
+ });
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 3. Registering Listeners
88
+
89
+ ### 3.1 Programmatic Subscriptions
90
+
91
+ ```javascript
92
+ const { events } = require('webspresso');
93
+
94
+ // 1. Basic Listener
95
+ const unsub = events.on('order.created', (order, meta) => {
96
+ console.log(`Order ${order.id} received at ${meta.timestamp}`);
97
+ });
98
+
99
+ // 2. Wildcard Listener
100
+ events.on('order.*', (data, meta) => {
101
+ console.log(`Order event ${meta.event} occurred`);
102
+ });
103
+
104
+ // 3. One-time Listener
105
+ events.once('app.ready', () => {
106
+ console.log('Application initialized');
107
+ });
108
+
109
+ // 4. Priority Ordering (higher runs first)
110
+ events.on('user.login', auditLogin, { priority: 100 });
111
+ events.on('user.login', notifyFriends, { priority: 0 });
112
+ ```
113
+
114
+ ### 3.2 File-Based Discovery (`listeners/` directory)
115
+
116
+ Webspresso auto-discovers listeners in the `listeners/` root directory:
117
+
118
+ ```javascript
119
+ // listeners/send-welcome-email.js
120
+ const { defineListener } = require('webspresso');
121
+
122
+ module.exports = defineListener('user.created', async (user, meta) => {
123
+ console.log(`Sending welcome email to ${user.email}`);
124
+ });
125
+ ```
126
+
127
+ Or multiple listeners in a single file:
128
+
129
+ ```javascript
130
+ // listeners/audit-events.js
131
+ module.exports = [
132
+ {
133
+ event: 'order.*',
134
+ priority: 50,
135
+ handle: async (data, meta) => {
136
+ console.log(`Audit: ${meta.event}`, data);
137
+ },
138
+ },
139
+ ];
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 4. Django-Style Signals
145
+
146
+ For decoupled domain events with strict sender boundaries:
147
+
148
+ ```javascript
149
+ const { defineSignal } = require('webspresso');
150
+
151
+ // Define signal
152
+ const invoicePaid = defineSignal('invoice_paid');
153
+
154
+ // Connect receiver with sender filter
155
+ invoicePaid.connect(
156
+ (invoice) => {
157
+ console.log(`Invoice ${invoice.id} processed for subscription`);
158
+ },
159
+ { sender: 'Subscription' }
160
+ );
161
+
162
+ // Trigger signal
163
+ invoicePaid.send({ id: 'INV-100' }, { sender: 'Subscription' });
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 5. Background Queue Bridging (`listenAndQueue`)
169
+
170
+ Connect application events directly to background jobs without manual boilerplate:
171
+
172
+ ```javascript
173
+ // In plugin or server setup:
174
+ app.events.listenAndQueue('order.created', 'send-order-confirmation', {
175
+ delay: 2000,
176
+ attempts: 3,
177
+ });
178
+ ```
179
+ When `order.created` is emitted, Webspresso automatically calls `req.queue.dispatch('send-order-confirmation', order, options)`.
package/.agents/ORM.md CHANGED
@@ -311,4 +311,153 @@ const mixed = await articleRepo.query()
311
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
312
  | **MySQL** | `'fts'` | `MATCH(...) AGAINST(? IN BOOLEAN MODE)` | Requires FULLTEXT index on table. |
313
313
 
314
+ ---
315
+
316
+ ## 9. Relational Cascading Soft-Delete & Selective Restore (`cascade: true`)
317
+
318
+ Webspresso ORM supports automated relational cascade operations for `hasMany` and `hasOne` relationships. When a parent record is soft-deleted or restored, child records are synchronized automatically.
319
+
320
+ ### 9.1 Relation Configuration
321
+
322
+ Configure `cascade` directly inside your model relations:
323
+
324
+ ```javascript
325
+ defineModel({
326
+ name: 'Author',
327
+ table: 'authors',
328
+ schema: zdb.schema({
329
+ id: zdb.id(),
330
+ name: zdb.string(),
331
+ deleted_at: zdb.timestamp({ nullable: true }),
332
+ }),
333
+ relations: {
334
+ // 1. Full cascade (soft-delete, hard-delete, and restore)
335
+ posts: {
336
+ type: 'hasMany',
337
+ model: () => 'Post', // or () => Post
338
+ foreignKey: 'author_id',
339
+ cascade: true,
340
+ },
341
+ // 2. hasOne cascade
342
+ profile: {
343
+ type: 'hasOne',
344
+ model: () => 'Profile',
345
+ foreignKey: 'author_id',
346
+ cascade: true,
347
+ },
348
+ // 3. Delete-only cascade (child is deleted with parent, but NOT restored)
349
+ tokens: {
350
+ type: 'hasMany',
351
+ model: () => 'ApiToken',
352
+ foreignKey: 'author_id',
353
+ cascade: 'delete', // or { delete: true, restore: false }
354
+ },
355
+ },
356
+ scopes: {
357
+ softDelete: true,
358
+ },
359
+ });
360
+ ```
361
+
362
+ ### 9.2 Cascade Deletion Behavior
363
+
364
+ - **Soft Delete (`repo.delete(id)`)**:
365
+ When an Author is soft-deleted, all active child records in cascading relations (`hasMany`, `hasOne`) are soft-deleted with the **exact same `deleted_at` timestamp**.
366
+ Cascade is recursive: If `Post` also defines `comments` with `cascade: true`, all comments under those posts are cascaded automatically.
367
+ - **Hard Delete (`repo.forceDelete(id)`)**:
368
+ When `forceDelete` is called (or when parent is a non-softDelete model), cascading child records are deleted **before** the parent to avoid foreign-key constraint violations (`FOREIGN KEY constraint failed`).
369
+ - **Cache Invalidation**:
370
+ The ORM query cache for all affected related models is automatically invalidated.
371
+
372
+ ### 9.3 Selective Restore (Smart Timestamp Matching)
373
+
374
+ A classic pitfall in soft-delete architectures is resurrecting child records that the user deliberately deleted prior to deleting the parent account:
375
+
376
+ ```
377
+ T1 (10:00): User manually deletes Post #1 (deleted_at = 10:00).
378
+ T2 (12:00): User account is soft-deleted (deleted_at = 12:00).
379
+ Post #2 and Post #3 receive deleted_at = 12:00.
380
+ T3 (15:00): User account is restored.
381
+ ```
382
+
383
+ By default, Webspresso ORM executes **selective restore**:
384
+ - It restores only child records where `child.deleted_at === parent.deleted_at` (Post #2 and Post #3).
385
+ - Post #1 remains soft-deleted because its `deleted_at` was `10:00`!
386
+
387
+ ```javascript
388
+ // Default: Safe selective restore (protects previously deleted children)
389
+ await authorRepo.restore(authorId);
390
+
391
+ // Optional: Force restore ALL children regardless of when they were deleted
392
+ await authorRepo.restore(authorId, { allChildren: true });
393
+
394
+ // Optional: Bypass cascading for a specific operation
395
+ await authorRepo.delete(authorId, { cascade: false });
396
+ await authorRepo.restore(authorId, { cascade: false });
397
+ ```
398
+
399
+ ### 9.4 Cycle & Recursion Safety
400
+
401
+ If your data model contains circular relations (e.g. `User.partner` <-> `Partner.user`), Webspresso ORM uses an internal visited set (`model:id`) to terminate recursion cleanly without stack overflows.
402
+
403
+ ---
404
+
405
+ ## 10. Django-inspired Auto-Migrations (`db:makemigrations`)
406
+
407
+ Webspresso supports automatic schema diffing against your live database, similar to Django's `makemigrations`. It inspects your `models/*.js` definitions, checks the current database schema, and automatically generates required Knex migration files (`createTable` or `alterTable` with `dropColumn` rollbacks):
408
+
409
+ ```bash
410
+ # Preview changes without writing files
411
+ npx webspresso db:makemigrations --dry-run
412
+
413
+ # Detect model changes and generate migration file
414
+ npx webspresso db:makemigrations
415
+
416
+ # Custom migration name
417
+ npx webspresso db:makemigrations --name add_profile_fields
418
+
419
+ # Apply migrations
420
+ npx webspresso db:migrate
421
+ ```
422
+
423
+ Programmatic usage via `db.migrate`:
424
+ ```javascript
425
+ // Check diff
426
+ const diff = await db.migrate.diff();
427
+ if (diff.hasChanges) {
428
+ console.log(diff.changes);
429
+ }
430
+
431
+ // Generate files
432
+ const result = await db.migrate.makeMigrations();
433
+ ```
434
+
435
+ ---
436
+
437
+ ## 11. AdonisJS-inspired Model Factories (`defineFactory`)
438
+
439
+ For unit testing, integration tests, and dev database seeding, Webspresso provides declarative Model Factories inspired by AdonisJS Lucid:
440
+
441
+ ```javascript
442
+ import { defineFactory } from 'webspresso';
443
+
444
+ export const UserFactory = defineFactory('User', ({ faker, index }) => ({
445
+ name: faker.person.fullName(),
446
+ email: faker.internet.email(),
447
+ role: 'user',
448
+ isActive: true,
449
+ }))
450
+ .state('admin', () => ({ role: 'admin' }))
451
+ .state('suspended', () => ({ isActive: false }));
452
+
453
+ // In-memory object generation (No DB hit)
454
+ const fakeUser = UserFactory.make({ name: 'Custom Name' });
455
+ const fakeUsers = UserFactory.count(5).make();
456
+
457
+ // Database persistence via Repository
458
+ const user = await UserFactory.create({ db });
459
+ const admins = await UserFactory.count(3).apply('admin').create({ db });
460
+ ```
461
+
462
+
314
463
 
@@ -112,7 +112,44 @@ Zero-dependency B2B & Developer API Key Management Plugin with standard prefix f
112
112
  - `getMigrationTemplate(tableName)`: Knex migration script creating `api_keys` table.
113
113
  - `createApiKeyModel(tableName)`: Webspresso ORM model definition with hidden `key_hash`.
114
114
 
115
- ### 2.2 `adminPanelPlugin` (`plugins/admin-panel`)
115
+ ### 2.3 `webhooksPlugin` (`plugins/webhooks`)
116
+ Outgoing Webhook Dispatcher and Management Plugin with zero external dependencies. Dispatches events to subscribed external HTTP endpoints, signs payloads with HMAC-SHA256 (`X-Webspresso-Signature` and standard timestamped `webhook-signature`), filters events with wildcards (`*`, `order.*`), automatically retries failed deliveries using exponential backoff, hooks directly into Webspresso Queue (`core/queue`), and provides an interactive Mithril.js Admin Panel management UI.
117
+ - **Options**:
118
+ - `db` / `knex`: Webspresso Database or Knex instance.
119
+ - `endpointsTable`: Database table for endpoints (default `'webhook_endpoints'`).
120
+ - `deliveriesTable`: Database table for delivery logs (default `'webhook_deliveries'`).
121
+ - `maxAttempts`: Maximum retry attempts on delivery failure (default `3`).
122
+ - `backoffBaseMs`: Base delay for exponential backoff in milliseconds (default `1000`).
123
+ - `timeoutMs`: HTTP request timeout in milliseconds (default `10000`).
124
+ - `wrapPayload`: Boolean (default `true`), wrap data in standard `{ id, event, timestamp, data }` event envelope.
125
+ - `queue`: Custom `QueueManager` instance (auto-detected from `ctx.app.queue` if present).
126
+ - `endpoints`: Static in-memory endpoints array (useful for testing or serverless setups).
127
+ - `apiPrefix`: Admin REST API prefix (default `'/webhooks'`).
128
+ - `includeDefaultPage`: Register Admin UI page and sidebar menu item (default `true`).
129
+ - `pageTitle`: Admin page title (default `'Webhooks'`).
130
+ - `menuOrder`: Admin sidebar order (default `92`).
131
+ - **Request & Application Context Injected**:
132
+ - `req.webhooks`: Direct reference to the `WebhookDispatcher` instance.
133
+ - `app.webhooks`: Direct reference on the Webspresso Application instance.
134
+ - **Built-in Services Registered**:
135
+ - `'webhook.dispatch'`: Dispatch events via `ctx.service('webhook.dispatch', { event, payload })`.
136
+ - `'webhook.ping'`: Test endpoint connectivity via `ctx.service('webhook.ping', { endpointId })`.
137
+ - `'webhook.retry'`: Manually retry a delivery via `ctx.service('webhook.retry', { deliveryId })`.
138
+ - **Programmatic API (`webhooksPlugin.api` / `dispatcher`)**:
139
+ - `dispatch(event, payload, options)`: Match endpoints and deliver payload asynchronously (or await with `wait: true`).
140
+ - `pingEndpoint(endpointId)`: Send a test `ping` event to verify destination connectivity.
141
+ - `retryDelivery(deliveryId)`: Re-send a failed delivery with incremented attempt count.
142
+ - `createEndpoint(data)`: Register a new webhook endpoint with custom or auto-generated `whsec_` secret.
143
+ - `updateEndpoint(id, data)` / `deleteEndpoint(id)`: Update or delete endpoint records.
144
+ - `listEndpoints(filters)`: List endpoints with delivery statistics.
145
+ - `listDeliveries(filters)`: Query delivery logs with filtering by endpoint, event, and success status.
146
+ - `purgeDeliveries(options)`: Purge logs older than specified retention days.
147
+ - `signPayload(payload, secret, options)`: Compute HMAC-SHA256 signature (`sha256=...` and `t=...,v1=...`).
148
+ - `verifySignature(payload, header, secret, options)`: Constant-time signature verification with drift tolerance.
149
+ - `getMigrationTemplate()`: Knex migration script for `webhook_endpoints` and `webhook_deliveries`.
150
+ - `getModels()`: Webspresso ORM model definitions for `WebhookEndpoint` and `WebhookDelivery`.
151
+
152
+ ### 2.4 `adminPanelPlugin` (`plugins/admin-panel`)
116
153
  Mithril.js SPA Admin Panel mounted at `/_admin`.
117
154
  - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
118
155
  - **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
@@ -131,14 +168,14 @@ Multipart file upload manager with local / cloud storage providers.
131
168
 
132
169
  ### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
133
170
  Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
134
- - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'Dosya Yöneticisi', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
171
+ - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
135
172
  - **Features**:
136
173
  - Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
137
174
  - Path traversal attack protection (`resolveSafePath`).
138
175
  - Grid (thumbnail gallery) & List (table) view modes.
139
176
  - Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
140
177
  - Media preview modal with copy URL and direct playback/preview.
141
- - "Kütüphaneden Seç" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
178
+ - "Choose from Media Library" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
142
179
  - Built-in services: `fileManager.list`, `fileManager.mkdir`, `fileManager.rename`, `fileManager.delete`, `fileManager.move`, `fileManager.upload`.
143
180
 
144
181
  ### 2.6 `corsPlugin` (`plugins/cors`)
@@ -256,19 +293,19 @@ Admin mutation tracking, record change timeline, and audit history with schedule
256
293
  - **Options**:
257
294
  - `db`: Database instance (must expose `.knex`).
258
295
  - `tableName`: Database table name (default `'audit_logs'`).
259
- - `registerRecordAction`: Register built-in "Zaman Tüneli" action on all admin models (default `true`).
296
+ - `registerRecordAction`: Register built-in "Timeline" action on all admin models (default `true`).
260
297
  - `retentionDays`: Days to retain audit trail entries (default `90`).
261
298
  - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
262
299
  - `autoCleanup`: Enable scheduled background purge (default `true`).
263
300
  - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
264
301
  - `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)**.
302
+ - **Timeline UI Features**:
303
+ - **Dual Mode**: Switch seamlessly between **Timeline View** and **Table View**.
267
304
  - **Record Focus Mode**: Clicking any `Model #ID` tag instantly filters and displays that specific record's revision timeline.
268
305
  - **Visual Spine & Status Icons**: Color-coded action nodes for `create` (emerald +), `update` (blue ✏️), `delete` (rose 🗑️), and `restore` (amber ↺).
269
306
  - **Changed Fields Diff**: Clearly lists modified fields (e.g. `title`, `price`) on every update event.
270
307
  - **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.
308
+ - **Built-in Record Action**: One-click "Timeline" button on record lists and edit forms jumps straight to that record's audit history.
272
309
  - **API & Admin Panel Endpoints**:
273
310
  - `GET /_admin/api/audit-logs`: List paginated logs with filters (`model`, `recordId`, `action`, `search`, `from`, `to`).
274
311
  - `GET /_admin/api/audit-logs/timeline/:model/:id`: Dedicated chronological record change timeline with summary stats.
package/.agents/QUEUE.md CHANGED
@@ -162,27 +162,30 @@ Webspresso includes a native, zero-dependency task scheduler with a POSIX-compli
162
162
 
163
163
  ### 1. Cron Expression Syntax
164
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`
165
+ - Wildcards: `*`, `?` (supported in day-of-month and day-of-week)
166
+ - Steps & Offsets: `*/5`, `10/15` (starting at minute 10, every 15 min), `10-30/5`
167
+ - Ranges: `1-5`, `5-7` (Fri-Sun), `MON-FRI`, `MON-SUN`, `JAN-DEC`
168
168
  - Lists: `1,15,30`, `MON,WED,FRI`
169
169
  - Descriptive aliases:
170
170
  - `@yearly`, `@annually` (`0 0 1 1 *`)
171
171
  - `@monthly` (`0 0 1 * *`)
172
172
  - `@weekly` (`0 0 * * 0`)
173
+ - `@weekdays`, `@workdays` (`0 0 * * 1-5`)
174
+ - `@weekends` (`0 0 * * 6,0`)
173
175
  - `@daily`, `@midnight` (`0 0 * * *`)
174
- - `@hourly` (`0 * * * *`)
176
+ - `@hourly`, `@every_hour` (`0 * * * *`)
175
177
  - `@every_minute`, `@minutely` (`* * * * *`)
176
178
  - `@every <N>m` (e.g. `@every 5m` → `*/5 * * * *`)
177
179
  - `@every <N>h` (e.g. `@every 2h` → `0 */2 * * *`)
180
+ - `@every <N>d` (e.g. `@every 2d` → `0 0 */2 * *`)
178
181
 
179
182
  ### 2. Programmatic Scheduling
180
183
  ```js
181
- // Schedule daily digest at 09:00 AM
184
+ // Schedule daily digest at 09:00 AM with Timezone support
182
185
  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
+ timezone: 'Europe/Istanbul', // IANA timezone support ('Europe/Istanbul', 'America/New_York')
187
+ overlap: false, // Prevent concurrent execution if previous run is still active (default: false)
188
+ maxRuns: 10, // Optional maximum executions before auto-unscheduling
186
189
  });
187
190
 
188
191
  // Or use the .cron() alias
@@ -190,9 +193,12 @@ queue.cron('@every 15m', 'metrics.collect');
190
193
 
191
194
  // Query schedules
192
195
  const schedules = queue.getSchedules();
193
- // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, runsCount, ... }]
196
+ // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, activeJobId, activeJobIds, runsCount, ... }]
194
197
 
195
- // Unschedule by schedule ID or job name
198
+ // Manually trigger a schedule immediately
199
+ await queue.triggerSchedule('reports.daily-digest');
200
+
201
+ // Unschedule by schedule ID or job name (removes all schedules matching the job name)
196
202
  queue.unschedule('metrics.collect');
197
203
  ```
198
204
 
@@ -201,8 +207,10 @@ queue.unschedule('metrics.collect');
201
207
  | :--- | :--- | :--- | :--- |
202
208
  | `id` | `string` | `schedule:${job}:${cron}` | Custom unique identifier |
203
209
  | `overlap` | `boolean` | `false` | When `false`, skips tick if a previous job run is still processing |
210
+ | `preventOverlap` | `boolean` | `true` | Alias for `overlap: false` |
204
211
  | `maxRuns` | `number` | `null` | Automatically unschedules task after N runs |
205
212
  | `utc` | `boolean` | `false` | Evaluate cron expressions in UTC instead of local timezone |
213
+ | `timezone` (`tz`) | `string` | `null` | IANA timezone name (e.g. `'Europe/Istanbul'`, `'America/New_York'`) |
206
214
  | `runImmediately` | `boolean` | `false` | Triggers a run right after scheduling |
207
215
 
208
216
  ### 4. File-Based Cron Discovery (`jobs/`)
@@ -400,9 +400,105 @@ Pages under `pages/` automatically inherit ancestor layouts:
400
400
  </div>
401
401
  ```
402
402
 
403
+ #### 3. Folder-Level Error Boundaries (`_error.njk` & `_error.js`)
404
+ When an exception occurs during SSR lifecycle execution (in `load()`, template rendering, hooks, or nested layout loaders):
405
+ - **Ancestral Discovery**: Webspresso searches up the directory hierarchy from the route's directory up to `pages/` for the nearest `_error.njk`.
406
+ - **Isolated Layout Preservation**: `_error.njk` is automatically wrapped inside the surrounding ancestor `_layout.njk` chain, keeping the UI intact (e.g. an error in `/dashboard/billing/invoices` renders the error card inside the dashboard sidebar and header).
407
+ - **Companion Loader (`_error.js`)**: Can export `load({ error, req, res, ctx, db })` to attach custom error data or report exceptions.
408
+ - **Status Code Propagation**: HTTP status is set to `error.status || error.statusCode || 500` (e.g., throwing `NotFoundError` returns HTTP 404).
409
+ - **Opt-Out**: Disable layout wrapping with `layout: false` in `_error.js` or frontmatter (`--- layout: false ---`).
410
+ - **Fallback**: If no `_error.njk` exists in any ancestor directory, execution falls through to the global Express error boundary (`app.setErrorHandler` / 500 page).
411
+
412
+ ```html
413
+ {# pages/dashboard/_error.njk #}
414
+ <div class="p-6 bg-red-50 dark:bg-red-950/30 rounded-xl border border-red-200">
415
+ <h2 class="text-xl font-bold text-red-700">Error {{ error.status }}</h2>
416
+ <p class="text-red-600 mt-2">{{ error.message }}</p>
417
+ {% if supportEmail %}<p class="mt-4 text-sm">Need help? Email {{ supportEmail }}</p>{% endif %}
418
+ </div>
419
+ ```
420
+
421
+ #### 4. Folder-Level Loading Skeletons (`_loading.njk`)
422
+ In SSR streaming mode (`stream: true` or `renderStream`):
423
+ - **Automatic Discovery**: Discovers the nearest `_loading.njk` up the directory chain.
424
+ - **Auto-Populate Slots**: Any empty `<div data-stream-target="slotName"></div>` in the page template is automatically populated with the `_loading.njk` skeleton HTML in the initial flushed shell chunk!
425
+ - **Context Availability**: Exposed as `{{ loadingSkeleton | safe }}` and `{{ loadingHtml | safe }}` in the template context.
426
+ - **Layout Integration**: Cascading `_layout.njk` templates automatically wrap the streaming shell.
427
+
428
+ ```html
429
+ {# pages/dashboard/_loading.njk #}
430
+ <div class="animate-pulse space-y-4">
431
+ <div class="h-8 bg-slate-200 dark:bg-slate-800 rounded w-1/4"></div>
432
+ <div class="h-32 bg-slate-200 dark:bg-slate-800 rounded"></div>
433
+ </div>
434
+ ```
435
+
436
+ ---
437
+
438
+ ## 7. Form Handling, Flash Messages & Old Input (AdonisJS & Django-inspired)
439
+
440
+ For classic SSR forms (POST-Redirect-GET), Webspresso provides built-in flash data propagation and Django-style ModelForm rendering helpers.
441
+
442
+ ### 7.1 Redirect with Old Input & Validation Errors
443
+ In your form submission handler (API or custom POST action):
444
+ ```javascript
445
+ // pages/api/auth/register.post.js
446
+ module.exports = {
447
+ handler: async (req, res) => {
448
+ const { email, password } = req.body;
449
+
450
+ if (!email.includes('@')) {
451
+ return res
452
+ .withInput() // Preserves req.body (sanitizes sensitive fields like password)
453
+ .withErrors({ email: 'Please enter a valid email address' })
454
+ .withFlash('warning', 'Please review the highlighted errors')
455
+ .redirect('/register');
456
+ }
457
+
458
+ await req.service('user.create', { email, password });
459
+ return res.withFlash('success', 'Account created! Please log in.').redirect('/login');
460
+ }
461
+ };
462
+ ```
463
+
464
+ ### 7.2 Nunjucks Template Helpers (`fsy`)
465
+ In your Nunjucks templates (`pages/*.njk`):
466
+ ```html
467
+ {% if fsy.hasFlash('success') %}
468
+ <div class="alert alert-success">{{ fsy.flash('success') }}</div>
469
+ {% endif %}
470
+
471
+ <form method="POST" action="/api/auth/register">
472
+ {{ fsy.csrf() | safe }}
473
+
474
+ <!-- Manual field with old value and inline error -->
475
+ <div class="mb-3">
476
+ <label class="form-label">Email</label>
477
+ <input type="email" name="email"
478
+ value="{{ fsy.old('email') }}"
479
+ class="form-control {% if fsy.hasError('email') %}is-invalid{% endif %}">
480
+ {% if fsy.hasError('email') %}
481
+ <div class="invalid-feedback">{{ fsy.error('email') }}</div>
482
+ {% endif %}
483
+ </div>
484
+
485
+ <!-- Or use the Django-inspired field helper (one-liner) -->
486
+ {{ fsy.field('email', { type: 'email', placeholder: 'you@example.com' }) | safe }}
487
+
488
+ <button type="submit" class="btn btn-primary">Register</button>
489
+ </form>
490
+ ```
491
+
492
+ ### 7.3 Django ModelForm Helper (`fsy.formFor`)
493
+ Generate an entire HTML form from an ORM model definition with automatic widgets and validation error mapping:
494
+ ```html
495
+ {# Automatically excludes id, created_at, updated_at and generates appropriate widgets #}
496
+ {{ fsy.formFor(req.db.getRepository('Article'), { action: '/articles', submitLabel: 'Publish' }) | safe }}
497
+ ```
498
+
403
499
  ---
404
500
 
405
- ## 7. Summary Checklist for Agents
501
+ ## 8. Summary Checklist for Agents
406
502
 
407
503
  When a user asks to add or modify endpoints:
408
504
  1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).