@almadar/std 16.181.0 → 16.182.0

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 (75) hide show
  1. package/behaviors/lolo/infra/atoms/std-calendar-sync.lolo +290 -0
  2. package/behaviors/lolo/infra/atoms/std-notification-center.lolo +290 -0
  3. package/behaviors/lolo/infra/atoms/std-notify-on-event.lolo +238 -40
  4. package/behaviors/lolo/ui/core/atoms/std-esign-flow.lolo +180 -3
  5. package/behaviors/lolo/ui/core/molecules/ui-calendar-grid.lolo +6 -0
  6. package/behaviors/lolo/ui/core/organisms/ui-detail-panel.lolo +4 -4
  7. package/behaviors/lolo/ui/game/.hand-authored.json +1 -1
  8. package/behaviors/lolo/ui/game/atoms/ui-control-grid.lolo +5 -0
  9. package/behaviors/lolo/ui/game/atoms/ui-game-hud.lolo +5 -0
  10. package/behaviors/lolo/ui/game/templates/ui-game-shell.lolo +5 -5
  11. package/behaviors/registry/infra/atoms/std-calendar-sync.orb +1643 -0
  12. package/behaviors/registry/infra/atoms/std-notification-center.orb +1621 -0
  13. package/behaviors/registry/infra/atoms/std-notify-on-event.orb +1734 -169
  14. package/behaviors/registry/ui/core/atoms/std-esign-flow.orb +846 -2
  15. package/behaviors/registry/ui/core/molecules/ui-calendar-grid.orb +9 -0
  16. package/behaviors/registry/ui/core/organisms/ui-detail-panel.orb +4 -2
  17. package/behaviors/registry/ui/game/atoms/ui-control-grid.orb +13 -1
  18. package/behaviors/registry/ui/game/atoms/ui-game-hud.orb +13 -1
  19. package/behaviors/registry/ui/game/templates/ui-game-shell.orb +5 -5
  20. package/canonical-operators.json +21 -1
  21. package/dist/behaviors/exports-reader.js +8303 -3043
  22. package/dist/behaviors/functions/index.d.ts +468 -10
  23. package/dist/behaviors/functions/index.js +18352 -13109
  24. package/dist/behaviors/index.d.ts +1 -1
  25. package/dist/behaviors/index.js +8304 -3044
  26. package/dist/behaviors/query.js +8303 -3043
  27. package/dist/behaviors-embeddings.hash.json +3 -3
  28. package/dist/behaviors-embeddings.json +449 -441
  29. package/dist/behaviors-registry.json +1164 -9
  30. package/dist/index.d.ts +3 -3
  31. package/dist/index.js +8275 -2978
  32. package/dist/knob-embeddings.hash.json +3 -3
  33. package/dist/knob-embeddings.json +1 -1
  34. package/dist/modules/anim.d.ts +1 -1
  35. package/dist/modules/array.d.ts +1 -1
  36. package/dist/modules/async.d.ts +1 -1
  37. package/dist/modules/behavior.d.ts +1 -1
  38. package/dist/modules/browser.d.ts +1 -1
  39. package/dist/modules/browser.js +21 -0
  40. package/dist/modules/composition.d.ts +1 -1
  41. package/dist/modules/contract.d.ts +1 -1
  42. package/dist/modules/core.d.ts +1 -1
  43. package/dist/modules/core.js +18 -2
  44. package/dist/modules/data.d.ts +1 -1
  45. package/dist/modules/ease.d.ts +1 -1
  46. package/dist/modules/format.d.ts +1 -1
  47. package/dist/modules/geo.d.ts +1 -1
  48. package/dist/modules/graph.d.ts +1 -1
  49. package/dist/modules/grid.d.ts +1 -1
  50. package/dist/modules/index.d.ts +1 -1
  51. package/dist/modules/index.js +39 -2
  52. package/dist/modules/integration.d.ts +1 -1
  53. package/dist/modules/llm.d.ts +1 -1
  54. package/dist/modules/math.d.ts +1 -1
  55. package/dist/modules/memory.d.ts +1 -1
  56. package/dist/modules/nn.d.ts +1 -1
  57. package/dist/modules/noise.d.ts +1 -1
  58. package/dist/modules/object.d.ts +1 -1
  59. package/dist/modules/os.d.ts +1 -1
  60. package/dist/modules/path.d.ts +1 -1
  61. package/dist/modules/prob.d.ts +1 -1
  62. package/dist/modules/session.d.ts +1 -1
  63. package/dist/modules/str.d.ts +1 -1
  64. package/dist/modules/tensor.d.ts +1 -1
  65. package/dist/modules/time.d.ts +1 -1
  66. package/dist/modules/trace.d.ts +1 -1
  67. package/dist/modules/train.d.ts +1 -1
  68. package/dist/modules/validate.d.ts +1 -1
  69. package/dist/modules/vector.d.ts +1 -1
  70. package/dist/modules/workspace.d.ts +1 -1
  71. package/dist/registry/factory-signatures.json +1 -1
  72. package/dist/registry.d.ts +1 -1
  73. package/dist/registry.js +39 -2
  74. package/dist/{types-DGJ6Y6nW.d.ts → types-BXWzEjFm.d.ts} +1 -1
  75. package/package.json +3 -3
@@ -1,6 +1,8 @@
1
- app std-notify-on-event "1.5.0"
2
- "std-notify-on-event — cross-orbital wiring atom. Subscribes to events from another orbital (e.g. CheckoutOrbital emits OrderPlaced, OrderRecordOrbital reacts via this atom) and dispatches a notification. Use when one orbital needs to react to or be notified about events that happen in another orbital — the wiring goes through this atom's `EventOccurred` listener which the molecule renames via traitOverrides.events to the upstream event name. Dispatch gates on `guardOp`: `eq` (default) does string-equality on `triggerStatus` against the upstream event's `status` field, exactly as before; `gt`/`lt`/`gte`/`lte` instead numerically compare `guardField` (read from the event's `data` payload) against `guardValue`, for threshold-crossing asks such as low-stock alerts. Resolves recipient from `recipients` (first entry; multi-recipient fan-out is a future extension), falling back to `fallbackRecipient` when set, or `@user.id` otherwise set `fallbackRecipient` for system/ops alerts (e.g. low-stock, shipping) so the acting user isn't notified in place of the intended audience. Skips dispatch when the recipient is in `suppressionList`. Emits in-app `notify` when `notifyChannel` is `\"in-app\"` (default, unchanged). `\"email\"`/`\"sms\"`/`\"webhook\"` dispatch through call-service at event time (email.send / twilio.sendSMS / webhook.send respectively) and persist a `NotificationRecord` audit row with an honest `dispatched` flag true only once the service call succeeds, false with the failure detail in `error` otherwise. `email` uses `emailSubject` (falls back to `template` when blank); `webhook` requires `webhookUrl` to actually dispatch (`webhookSecret` signs the payload) a blank `webhookUrl` still records the audit row undispatched rather than failing."
3
- type QuietHoursSpec = { start : string, end : string }
1
+ app std-notify-on-event "2.0.0"
2
+ "std-notify-on-event — cross-orbital wiring atom. Subscribes to events from another orbital (e.g. CheckoutOrbital emits OrderPlaced, OrderRecordOrbital reacts via this atom) and dispatches a notification. Use when one orbital needs to react to or be notified about events that happen in another orbital — the wiring goes through this atom's `EventOccurred` listener which the molecule renames via traitOverrides.events to the upstream event name. Dispatch gates on `guardOp`: `eq` (default) does string-equality on `triggerStatus` against the upstream event's `status` field; `gt`/`lt`/`gte`/`lte` instead numerically compare `guardField` (read from the event's `data` payload) against `guardValue`, for threshold-crossing asks such as low-stock alerts. Resolves recipient from `recipients` (first entry; multi-recipient fan-out is a future extension), falling back to `fallbackRecipient` when set, or `@user.id` otherwise. Skips dispatch when the recipient is in `suppressionList`. Delivery consults std-notification-center data before dispatching: per-user NotificationPreference rows (matched on `userId` = recipient and `eventType` = this listener's `eventKey`, blank = all) can mute (`mode: off`), defer to the digest drain (`mode: digest`), override the channel, or impose per-user quiet hours; the atom-level `quietStartHour`/`quietEndHour` window and the `frequencyCapPerWeek` rolling cap (counted against dispatched NotificationRecord audit rows for the same recipient + channel) suppress likewise. Every suppression persists an honest audit row (`dispatched: false` with the reason in `error`: quiet-hours / digest-deferred / muted-by-preference / frequency-cap / no-push-subscription) which std-notification-center's digest cycle drains later. Channels: `in-app` emits the `notify` toast; `email`/`sms`/`webhook` dispatch through call-service (email.send / twilio.sendSMS / webhook.send); `push` looks up the recipient's PushSubscription row (owned by std-notification-center) and dispatches web-push via push.send (`pushTitle` falls back to `template`; `pushDeepLink`/`pushIcon` optional). The audit row's `dispatched` flag is honest true only once the service call succeeds."
3
+ type PrefRow = { userId : string, eventType : string, mode : string, channel : string, quietStartHour : number, quietEndHour : number }
4
+ type AuditRow = { recipient : string, channel : string, dispatched : boolean, recordedAt : number }
5
+ type SubRow = { userId : string, endpoint : string, p256dh : string, auth : string }
4
6
  orbital NotifyOnEventOrbital {
5
7
  type EventOccurred = Event { data : object, status : string } "Rename via traitOverrides.events to the upstream event (e.g. ContactSaved). The atom listens on this sentinel and dispatches a notification. The optional `status` field lets `triggerStatus` gate dispatch to a single status-change value."
6
8
 
@@ -13,8 +15,34 @@ orbital NotifyOnEventOrbital {
13
15
  message : string @description "The content of the notification message." @synonyms "text, content, body" = ""
14
16
  severity : string @description "Indicates the importance or urgency level of the notification." @synonyms "priority, level, importance" = info
15
17
  dispatched : boolean @description "Indicates whether the notification has been successfully sent." @synonyms "sent, delivered, status" = false
16
- error : string @description "Failure detail from the delivery service when dispatched is false (email/sms/webhook channels only)." @synonyms "error message, failure reason, dispatch error" = ""
18
+ error : string @description "Failure detail from the delivery service when dispatched is false, or the suppression reason (quiet-hours / digest-deferred / muted-by-preference / frequency-cap / no-push-subscription)." @synonyms "error message, failure reason, dispatch error" = ""
17
19
  recordedAt : number @description "Timestamp indicating when the notification record was created." @synonyms "creation time, timestamp, date, when" = 0
20
+ prefMode : string @description "Scratch: effective preference mode resolved for the current dispatch (instant/digest/off)." @synonyms "resolved mode" = ""
21
+ prefChannel : string @description "Scratch: channel override from the matched preference row; blank = no override." @synonyms "channel override" = ""
22
+ prefQuietStart : number @description "Scratch: per-user quiet-hours start (hour 0-23) from the matched preference row." @synonyms "user quiet start" = 0
23
+ prefQuietEnd : number @description "Scratch: per-user quiet-hours end (hour 0-23) from the matched preference row." @synonyms "user quiet end" = 0
24
+ pushEndpoint : string @description "Scratch: push endpoint of the recipient's subscription for the current dispatch." @synonyms "subscription endpoint" = ""
25
+ pushP256dh : string @description "Scratch: p256dh key of the recipient's push subscription." @synonyms "push key" = ""
26
+ pushAuth : string @description "Scratch: auth secret of the recipient's push subscription." @synonyms "push auth" = ""
27
+ }
28
+
29
+ entity NotificationPreference [persistent: notificationPreferences] {
30
+ id : string!
31
+ userId : string @description "Owner of this preference row." @synonyms "user, owner, account" = ""
32
+ eventType : string @description "Event key this preference applies to (matches the listener's eventKey); blank = all events." @synonyms "event, trigger, subscription" = ""
33
+ mode : string @description "Delivery mode: instant, digest, or off." @synonyms "delivery mode, frequency, cadence" = instant
34
+ channel : string @description "Channel override for this user (in-app/email/sms/push/webhook); blank = the listener's configured channel." @synonyms "preferred channel, delivery method" = ""
35
+ quietStartHour : number @description "Per-user quiet-hours start (hour 0-23); equal to quietEndHour = disabled." @synonyms "quiet start, do not disturb from" = 0
36
+ quietEndHour : number @description "Per-user quiet-hours end (hour 0-23); equal to quietStartHour = disabled." @synonyms "quiet end, do not disturb until" = 0
37
+ }
38
+
39
+ entity PushSubscription [persistent: pushSubscriptions] {
40
+ id : string!
41
+ userId : string @description "Owner of this browser push subscription." @synonyms "user, owner, account" = ""
42
+ endpoint : string @description "Push service endpoint URL for this subscription." @synonyms "push url, subscription endpoint" = ""
43
+ p256dh : string @description "Client public key (p256dh) of the subscription." @synonyms "public key" = ""
44
+ auth : string @description "Auth secret of the subscription." @synonyms "auth secret" = ""
45
+ createdAt : number @description "When this subscription was captured." @synonyms "subscribed at, created" = 0
18
46
  }
19
47
 
20
48
  trait NotifyOnEventListener -> NotificationRecord [lifecycle, instance] {
@@ -26,35 +54,106 @@ orbital NotifyOnEventOrbital {
26
54
  (set @entity.channel @config.notifyChannel)
27
55
  (set @entity.message @config.template)
28
56
  (set @entity.severity @config.severity)
29
- (set @entity.dispatched (and ["=", @config.notifyChannel, in-app] (not (array/includes @config.suppressionList @entity.recipient))))
30
- (set @entity.recordedAt 0)
31
- (when (not (array/includes @config.suppressionList @entity.recipient)) (if ["=", @config.notifyChannel, in-app] (notify @config.severity @config.template) (if ["=", @config.notifyChannel, email] (call-service "email" "send" {
32
- body: @config.template
57
+ (set @entity.dispatched false)
58
+ (set @entity.recordedAt @now)
59
+ (when (not (array/includes @config.suppressionList @entity.recipient)) (if (and (not ["=", @config.quietStartHour, @config.quietEndHour]) (if ["<", @config.quietStartHour, @config.quietEndHour] (and [">=", (time/hour @now), @config.quietStartHour] ["<", (time/hour @now), @config.quietEndHour]) (or [">=", (time/hour @now), @config.quietStartHour] ["<", (time/hour @now), @config.quietEndHour]))) (persist create NotificationRecord {
60
+ channel: @entity.channel
61
+ dispatched: false
62
+ error: "quiet-hours"
63
+ message: @entity.message
64
+ recipient: @entity.recipient
65
+ recordedAt: @now
66
+ severity: @entity.severity
67
+ }) (fetch NotificationPreference { emit: { failure: NOTIFY_LOOKUP_FAILED, success: NOTIFY_PREFS_LOADED } })))
68
+ (emit NotificationDispatched {
69
+ channel: @entity.channel
70
+ message: @entity.message
71
+ recipient: @entity.recipient
72
+ severity: @entity.severity
73
+ })
74
+ NOTIFY_PREFS_LOADED -> idle
75
+ (set @entity.prefMode (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] "instant" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) mode)))
76
+ (set @entity.prefChannel (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] "" (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) channel)))
77
+ (set @entity.prefQuietStart (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] 0 (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) quietStartHour)))
78
+ (set @entity.prefQuietEnd (if ["=", (array/len (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))), 0] 0 (object/get (array/first (array/filter ?data (fn p (and ["=", (object/get @p userId), @entity.recipient] (or ["=", (object/get @p eventType), ""] ["=", (object/get @p eventType), @config.eventKey]))))) quietEndHour)))
79
+ (set @entity.channel (if ["=", @entity.prefChannel, ""] @entity.channel @entity.prefChannel))
80
+ (if ["=", @entity.prefMode, "off"] (persist create NotificationRecord {
81
+ channel: @entity.channel
82
+ dispatched: false
83
+ error: "muted-by-preference"
84
+ message: @entity.message
85
+ recipient: @entity.recipient
86
+ recordedAt: @now
87
+ severity: @entity.severity
88
+ }) (if ["=", @entity.prefMode, "digest"] (persist create NotificationRecord {
89
+ channel: @entity.channel
90
+ dispatched: false
91
+ error: "digest-deferred"
92
+ message: @entity.message
93
+ recipient: @entity.recipient
94
+ recordedAt: @now
95
+ severity: @entity.severity
96
+ }) (if (and (not ["=", @entity.prefQuietStart, @entity.prefQuietEnd]) (if ["<", @entity.prefQuietStart, @entity.prefQuietEnd] (and [">=", (time/hour @now), @entity.prefQuietStart] ["<", (time/hour @now), @entity.prefQuietEnd]) (or [">=", (time/hour @now), @entity.prefQuietStart] ["<", (time/hour @now), @entity.prefQuietEnd]))) (persist create NotificationRecord {
97
+ channel: @entity.channel
98
+ dispatched: false
99
+ error: "quiet-hours"
100
+ message: @entity.message
101
+ recipient: @entity.recipient
102
+ recordedAt: @now
103
+ severity: @entity.severity
104
+ }) (if ["=", @entity.channel, in-app] (do (set @entity.dispatched true) (notify @config.severity @config.template)) (fetch NotificationRecord {
105
+ emit: { failure: NOTIFY_LOOKUP_FAILED, success: NOTIFY_CAP_LOADED }
106
+ filter: [">", (object/get @entity recordedAt), ["-", @now, 604800000]]
107
+ })))))
108
+ NOTIFY_CAP_LOADED -> idle
109
+ (if (and [">", @config.frequencyCapPerWeek, 0] [">=", (array/len (array/filter ?data (fn r (and ["=", (object/get @r recipient), @entity.recipient] ["=", (object/get @r channel), @entity.channel] ["=", (object/get @r dispatched), true])))), @config.frequencyCapPerWeek]) (persist create NotificationRecord {
110
+ channel: @entity.channel
111
+ dispatched: false
112
+ error: "frequency-cap"
113
+ message: @entity.message
114
+ recipient: @entity.recipient
115
+ recordedAt: @now
116
+ severity: @entity.severity
117
+ }) (if ["=", @entity.channel, email] (call-service "email" "send" {
118
+ body: @entity.message
33
119
  from: ""
34
- subject: (if ["=", @config.emailSubject, ""] @config.template @config.emailSubject)
120
+ subject: (if ["=", @config.emailSubject, ""] @entity.message @config.emailSubject)
35
121
  to: @entity.recipient
36
- } { emit: { failure: NOTIFY_EMAIL_FAILED, success: NOTIFY_EMAIL_SENT } }) (if ["=", @config.notifyChannel, sms] (call-service "twilio" "sendSMS" {
37
- body: @config.template
122
+ } { emit: { failure: NOTIFY_EMAIL_FAILED, success: NOTIFY_EMAIL_SENT } }) (if ["=", @entity.channel, sms] (call-service "twilio" "sendSMS" {
123
+ body: @entity.message
38
124
  to: @entity.recipient
39
- } { emit: { failure: NOTIFY_SMS_FAILED, success: NOTIFY_SMS_SENT } }) (if (not ["=", @config.webhookUrl, ""]) (call-service "webhook" "send" {
40
- event: @config.template
41
- payload: { message: @config.template, recipient: @entity.recipient, severity: @config.severity }
125
+ } { emit: { failure: NOTIFY_SMS_FAILED, success: NOTIFY_SMS_SENT } }) (if ["=", @entity.channel, push] (fetch PushSubscription { emit: { failure: NOTIFY_LOOKUP_FAILED, success: NOTIFY_PUSH_SUBS } }) (if (not ["=", @config.webhookUrl, ""]) (call-service "webhook" "send" {
126
+ event: @entity.message
127
+ payload: { message: @entity.message, recipient: @entity.recipient, severity: @entity.severity }
42
128
  secret: @config.webhookSecret
43
129
  url: @config.webhookUrl
44
130
  } { emit: { failure: NOTIFY_WEBHOOK_FAILED, success: NOTIFY_WEBHOOK_SENT } }) (persist create NotificationRecord {
45
- channel: @config.notifyChannel
131
+ channel: @entity.channel
46
132
  dispatched: false
47
- message: @config.template
133
+ message: @entity.message
48
134
  recipient: @entity.recipient
49
- recordedAt: 0
50
- severity: @config.severity
135
+ recordedAt: @now
136
+ severity: @entity.severity
51
137
  }))))))
52
- (emit NotificationDispatched {
53
- channel: @config.notifyChannel
54
- message: @config.template
138
+ NOTIFY_PUSH_SUBS -> idle
139
+ (set @entity.pushEndpoint (if ["=", (array/len (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))), 0] "" (object/get (array/first (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))) endpoint)))
140
+ (set @entity.pushP256dh (if ["=", (array/len (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))), 0] "" (object/get (array/first (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))) p256dh)))
141
+ (set @entity.pushAuth (if ["=", (array/len (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))), 0] "" (object/get (array/first (array/filter ?data (fn s ["=", (object/get @s userId), @entity.recipient]))) auth)))
142
+ (if ["=", @entity.pushEndpoint, ""] (persist create NotificationRecord {
143
+ channel: @entity.channel
144
+ dispatched: false
145
+ error: "no-push-subscription"
146
+ message: @entity.message
55
147
  recipient: @entity.recipient
56
- severity: @config.severity
57
- })
148
+ recordedAt: @now
149
+ severity: @entity.severity
150
+ }) (call-service "push" "send" {
151
+ subscription: { endpoint: @entity.pushEndpoint, keys: { auth: @entity.pushAuth, p256dh: @entity.pushP256dh } }
152
+ title: (if ["=", @config.pushTitle, ""] @entity.message @config.pushTitle)
153
+ body: @entity.message
154
+ url: @config.pushDeepLink
155
+ icon: @config.pushIcon
156
+ } { emit: { failure: NOTIFY_PUSH_FAILED, success: NOTIFY_PUSH_SENT } }))
58
157
  NOTIFY_EMAIL_SENT -> idle
59
158
  (set @entity.dispatched true)
60
159
  (persist create NotificationRecord {
@@ -62,7 +161,7 @@ orbital NotifyOnEventOrbital {
62
161
  dispatched: true
63
162
  message: @entity.message
64
163
  recipient: @entity.recipient
65
- recordedAt: 0
164
+ recordedAt: @now
66
165
  severity: @entity.severity
67
166
  })
68
167
  NOTIFY_EMAIL_FAILED -> idle
@@ -74,7 +173,7 @@ orbital NotifyOnEventOrbital {
74
173
  error: @entity.error
75
174
  message: @entity.message
76
175
  recipient: @entity.recipient
77
- recordedAt: 0
176
+ recordedAt: @now
78
177
  severity: @entity.severity
79
178
  })
80
179
  NOTIFY_SMS_SENT -> idle
@@ -84,7 +183,7 @@ orbital NotifyOnEventOrbital {
84
183
  dispatched: true
85
184
  message: @entity.message
86
185
  recipient: @entity.recipient
87
- recordedAt: 0
186
+ recordedAt: @now
88
187
  severity: @entity.severity
89
188
  })
90
189
  NOTIFY_SMS_FAILED -> idle
@@ -96,7 +195,7 @@ orbital NotifyOnEventOrbital {
96
195
  error: @entity.error
97
196
  message: @entity.message
98
197
  recipient: @entity.recipient
99
- recordedAt: 0
198
+ recordedAt: @now
100
199
  severity: @entity.severity
101
200
  })
102
201
  NOTIFY_WEBHOOK_SENT -> idle
@@ -106,7 +205,7 @@ orbital NotifyOnEventOrbital {
106
205
  dispatched: true
107
206
  message: @entity.message
108
207
  recipient: @entity.recipient
109
- recordedAt: 0
208
+ recordedAt: @now
110
209
  severity: @entity.severity
111
210
  })
112
211
  NOTIFY_WEBHOOK_FAILED -> idle
@@ -118,7 +217,41 @@ orbital NotifyOnEventOrbital {
118
217
  error: @entity.error
119
218
  message: @entity.message
120
219
  recipient: @entity.recipient
121
- recordedAt: 0
220
+ recordedAt: @now
221
+ severity: @entity.severity
222
+ })
223
+ NOTIFY_PUSH_SENT -> idle
224
+ (set @entity.dispatched true)
225
+ (persist create NotificationRecord {
226
+ channel: @entity.channel
227
+ dispatched: true
228
+ message: @entity.message
229
+ recipient: @entity.recipient
230
+ recordedAt: @now
231
+ severity: @entity.severity
232
+ })
233
+ NOTIFY_PUSH_FAILED -> idle
234
+ (set @entity.dispatched false)
235
+ (set @entity.error ?error)
236
+ (persist create NotificationRecord {
237
+ channel: @entity.channel
238
+ dispatched: false
239
+ error: @entity.error
240
+ message: @entity.message
241
+ recipient: @entity.recipient
242
+ recordedAt: @now
243
+ severity: @entity.severity
244
+ })
245
+ NOTIFY_LOOKUP_FAILED -> idle
246
+ (set @entity.dispatched false)
247
+ (set @entity.error ?error)
248
+ (persist create NotificationRecord {
249
+ channel: @entity.channel
250
+ dispatched: false
251
+ error: @entity.error
252
+ message: @entity.message
253
+ recipient: @entity.recipient
254
+ recordedAt: @now
122
255
  severity: @entity.severity
123
256
  })
124
257
  }
@@ -130,6 +263,21 @@ orbital NotifyOnEventOrbital {
130
263
  message : string
131
264
  severity : string
132
265
  }
266
+ NOTIFY_PREFS_LOADED {
267
+ data : [PrefRow]
268
+ }
269
+ @description "NotificationPreference rows loaded; the arm resolves the recipient's effective mode/channel/quiet-hours"
270
+ @tier "secondary"
271
+ NOTIFY_CAP_LOADED {
272
+ data : [AuditRow]
273
+ }
274
+ @description "Rolling 7-day NotificationRecord window loaded; the arm enforces frequencyCapPerWeek then dispatches"
275
+ @tier "secondary"
276
+ NOTIFY_PUSH_SUBS {
277
+ data : [SubRow]
278
+ }
279
+ @description "PushSubscription rows loaded; the arm picks the recipient's subscription and dispatches push.send"
280
+ @tier "secondary"
133
281
  NOTIFY_EMAIL_SENT {
134
282
  id : string!
135
283
  status : string!
@@ -164,6 +312,23 @@ orbital NotifyOnEventOrbital {
164
312
  }
165
313
  @description "webhook.send service call failed — dispatched lands false and error carries the service detail"
166
314
  @tier "secondary"
315
+ NOTIFY_PUSH_SENT {
316
+ statusCode : number!
317
+ ok : boolean!
318
+ expired : boolean!
319
+ }
320
+ @description "push.send service result — expired:true means the subscription is gone and should be pruned"
321
+ @tier "secondary"
322
+ NOTIFY_PUSH_FAILED {
323
+ error : string!
324
+ }
325
+ @description "push.send service call failed — dispatched lands false and error carries the service detail"
326
+ @tier "secondary"
327
+ NOTIFY_LOOKUP_FAILED {
328
+ error : string!
329
+ }
330
+ @description "A preference / cap-window / subscription fetch failed — dispatched lands false with the fetch error"
331
+ @tier "secondary"
167
332
  }
168
333
 
169
334
  listens {
@@ -171,6 +336,15 @@ orbital NotifyOnEventOrbital {
171
336
  data : object
172
337
  status : string
173
338
  }
339
+ NOTIFY_PREFS_LOADED {
340
+ data : [PrefRow]
341
+ }
342
+ NOTIFY_CAP_LOADED {
343
+ data : [AuditRow]
344
+ }
345
+ NOTIFY_PUSH_SUBS {
346
+ data : [SubRow]
347
+ }
174
348
  }
175
349
 
176
350
  config {
@@ -199,16 +373,36 @@ orbital NotifyOnEventOrbital {
199
373
  @description "Event names that should fire a notification. Set this to the upstream event name (e.g. OrderPlaced, CheckoutCompleted) so this atom listens for events from the orbital that emits them. The orbital owning this trait reacts whenever the named event fires anywhere in the schema."
200
374
  @synonyms "subscribe to event, listen for event, react to event, watch for, wire upstream event, observe event from another orbital, cross-orbital subscription, on-event trigger"
201
375
  @tier "internal"
202
- notifyChannel : "in-app" | "email" | "sms" | "webhook" = in-app
376
+ eventKey : string = ""
377
+ @label "Event key for preferences"
378
+ @description "Stable key naming the upstream event this listener is bound to (e.g. 'OrderPlaced'). NotificationPreference rows (std-notification-center) whose eventType equals this key — or is blank — apply to this listener's dispatches."
379
+ @synonyms "preference key, event name, subscription key, notification topic"
380
+ @tier "policy"
381
+ notifyChannel : "in-app" | "email" | "sms" | "push" | "webhook" = in-app
203
382
  @label "Channel"
204
- @description "Delivery channel. `in-app` (default) emits the in-app `notify` toast — unchanged from prior versions. `email`/`sms`/`webhook` dispatch through call-service at event time (email.send / twilio.sendSMS / webhook.send) and persist a NotificationRecord audit row with an honest `dispatched` flag: true only once the service call succeeds, false plus the failure detail in `error` otherwise. `webhook` requires `webhookUrl` to actually dispatch; a blank `webhookUrl` still records the audit row undispatched rather than failing."
205
- @synonyms "delivery method, notification channel, send via, dispatch through"
383
+ @description "Delivery channel. `in-app` (default) emits the in-app `notify` toast. `email`/`sms`/`webhook` dispatch through call-service at event time (email.send / twilio.sendSMS / webhook.send) and persist a NotificationRecord audit row with an honest `dispatched` flag: true only once the service call succeeds, false plus the failure detail in `error` otherwise. `push` looks up the recipient's PushSubscription row (captured by std-notification-center's subscribe flow) and dispatches web-push via push.send — a recipient with no subscription records the audit row undispatched (`no-push-subscription`). `webhook` requires `webhookUrl` to actually dispatch; a blank `webhookUrl` still records the audit row undispatched rather than failing. A per-user NotificationPreference row with a non-blank `channel` overrides this value for that recipient."
384
+ @synonyms "delivery method, notification channel, send via, dispatch through, push notification, web push"
206
385
  @tier "policy"
207
386
  emailSubject : string = ""
208
387
  @label "Email subject"
209
388
  @description "Subject line used for the email channel. Falls back to `template` when blank. Ignored for other channels."
210
389
  @synonyms "subject, subject line, email title, mail subject"
211
390
  @tier "presentation"
391
+ pushTitle : string = ""
392
+ @label "Push title"
393
+ @description "Notification title for the push channel. Falls back to `template` when blank. Ignored for other channels."
394
+ @synonyms "notification title, push heading, alert title"
395
+ @tier "presentation"
396
+ pushDeepLink : string = ""
397
+ @label "Push deep link"
398
+ @description "In-app path opened when the recipient clicks the push notification (e.g. '/orders'). Blank = the app root."
399
+ @synonyms "click url, open on click, target path, notification link"
400
+ @tier "presentation"
401
+ pushIcon : string = ""
402
+ @label "Push icon"
403
+ @description "Icon URL shown on the push notification. Blank = the browser default."
404
+ @synonyms "notification icon, badge image"
405
+ @tier "presentation"
212
406
  webhookUrl : string = ""
213
407
  @label "Webhook URL"
214
408
  @description "Destination endpoint for the webhook channel. Dispatch only fires when this is non-blank; a blank URL on channel \"webhook\" records the audit row with dispatched:false instead of crashing (a consumer misconfiguration, not a failure)."
@@ -244,19 +438,23 @@ orbital NotifyOnEventOrbital {
244
438
  @description "Recipient ids/addresses to skip (e.g. unsubscribed users). Suppression check runs before dispatch."
245
439
  @synonyms "block list, unsubscribed, opt-out list"
246
440
  @tier "policy"
247
- quietHours : QuietHoursSpec = {
248
- end: ""
249
- start: ""
250
- }
251
- @label "Quiet hours"
252
- @description "Local-time window during which dispatch is suppressed. Both start and end required to activate. WIRING DEFERRED: needs @now-aware time comparison primitive (gap — docs/Almadar_Std_Factories.md)."
441
+ quietStartHour : number = 0
442
+ @label "Quiet hours start"
443
+ @description "Hour of day (0-23) when the atom-level quiet window opens. Equal to `quietEndHour` = disabled. Dispatches inside the window persist an audit row (`error: quiet-hours`, dispatched:false) which std-notification-center's digest cycle drains later. Per-user quiet hours from NotificationPreference rows apply in addition."
444
+ @synonyms "quiet hours, do not disturb from, silence start, night mode start"
445
+ @tier "policy"
446
+ quietEndHour : number = 0
447
+ @label "Quiet hours end"
448
+ @description "Hour of day (0-23) when the atom-level quiet window closes. Equal to `quietStartHour` = disabled."
449
+ @synonyms "do not disturb until, silence end, night mode end"
253
450
  @tier "policy"
254
451
  frequencyCapPerWeek : number = 0
255
452
  @label "Frequency cap (per week)"
256
- @description "Max sends per (channel, recipient) per rolling 7-day window. 0 = uncapped. WIRING DEFERRED: needs stateful history query (gap docs/Almadar_Std_Factories.md)."
453
+ @description "Max dispatched sends per (channel, recipient) per rolling 7-day window, counted against dispatched NotificationRecord audit rows. 0 = uncapped. Applies to the persisted channels (email/sms/push/webhook); in-app toasts are not audited and not capped. A capped dispatch persists an audit row (`error: frequency-cap`, dispatched:false)."
454
+ @synonyms "rate limit, cap, max per week, throttle, send limit"
257
455
  @tier "policy"
258
456
  }
259
457
  }
260
458
 
261
459
  page "/notify-on-event" as NotifyOnEventPage -> NotifyOnEventListener
262
- }
460
+ }
@@ -1,5 +1,5 @@
1
- app std-esign-flow "1.1.0"
2
- "std-esign-flow — the send-for-signature flow: list signature requests, create one (pick document + recipient), resend or cancel, track status. Composes std-file-store + std-esign-request; no document-rendering or code-preview chrome."
1
+ app std-esign-flow "1.2.0"
2
+ "std-esign-flow — the send-for-signature flow: list signature requests, create one (pick document + recipient), resend or cancel, track status. Composes std-file-store + std-esign-request; no document-rendering or code-preview chrome. v1.2 (W5): EnvelopeDispatcher wires sessions to the real `esign` service — SEND_ENVELOPE dispatches a DocuSign envelope (the document rides the event as an UploadDropZone-shaped file payload, since StoredFile rows are metadata-only) and stamps `envelopeId`/`sentAt`/status=sent; the browse grid's per-row \"Refresh status\" pulls getEnvelopeStatus and maps completed/delivered/declined onto signed/viewed/declined."
3
3
  orbital SignatureSessionOrbital {
4
4
  uses StoredFile from "std/behaviors/std-file-store"
5
5
 
@@ -23,6 +23,7 @@ orbital SignatureSessionOrbital {
23
23
  status : "draft" | "sent" | "viewed" | "signed" | "declined" @description "Current state of the signature request." @synonyms "state, condition, stage" = draft
24
24
  sentAt : datetime @description "Timestamp indicating when the request was sent." @synonyms "sent date, submission time, dispatched"
25
25
  signedAt : datetime @description "The date and time the signature was applied." @synonyms "signed date, completion date, signature time"
26
+ envelopeId : string @description "Provider envelope id stamped when the real e-sign dispatch succeeds; blank = never dispatched. Written by EnvelopeDispatcher, not user-facing." @synonyms "docusign id, provider reference" = ""
26
27
  pendingId : string @description "Unique identifier for a pending action or task." @synonyms "taskId, pendingKey, referenceId" = ""
27
28
  }
28
29
 
@@ -102,6 +103,10 @@ orbital SignatureSessionOrbital {
102
103
  event: REQUEST_DELETE
103
104
  label: Cancel
104
105
  variant: danger
106
+ }, {
107
+ event: CHECK_ENVELOPE_STATUS
108
+ label: "Refresh status"
109
+ variant: secondary
105
110
  }]
106
111
  type: data-grid
107
112
  }]
@@ -174,6 +179,13 @@ orbital SignatureSessionOrbital {
174
179
  @description "Indicates a signature request has been resent."
175
180
  @synonyms "resend, retransmit, repeat"
176
181
  @tier "presentation"
182
+ CHECK_ENVELOPE_STATUS {
183
+ id : string!
184
+ row : SignatureSession
185
+ }
186
+ @description "Pull the provider's current envelope status for this request."
187
+ @synonyms "refresh status, check signature, poll envelope"
188
+ @tier "domain"
177
189
  REQUEST_DELETE {
178
190
  id : string!
179
191
  row : SignatureSession
@@ -339,5 +351,170 @@ orbital SignatureSessionOrbital {
339
351
  }
340
352
  }
341
353
 
342
- page "/esign-flow" as SignatureSessionPage -> SignatureSessionBrowse, SignatureSessionCreate, SignatureSessionDelete, SignatureSessionPersistor
354
+ ;; W5: the real e-sign wire. SEND_ENVELOPE dispatches a provider envelope
355
+ ;; the document travels IN the event as an UploadDropZone-shaped file payload
356
+ ;; (StoredFile rows are metadata-only, so there is nothing to fetch); the
357
+ ;; session row is stamped envelopeId/sentAt/status=sent on success. The
358
+ ;; browse grid's "Refresh status" action pulls the provider status and maps
359
+ ;; completed/delivered/declined onto the session's signed/viewed/declined.
360
+ trait EnvelopeDispatcher -> SignatureSession [lifecycle, instance] {
361
+ initial: idle
362
+
363
+ state idle {
364
+ SEND_ENVELOPE -> dispatching
365
+ (set @entity.id ?id)
366
+ (call-service esign sendEnvelope {
367
+ documentContent: (object/get ?file content)
368
+ documentName: (object/get ?row documentName)
369
+ emailSubject: (str/concat "Signature requested: " (object/get ?row documentName))
370
+ recipientEmail: (object/get ?row recipientEmail)
371
+ recipientName: (object/get ?row recipientName)
372
+ } {
373
+ emit: { failure: ENVELOPE_SEND_FAILED, success: ENVELOPE_SENT }
374
+ })
375
+ CHECK_ENVELOPE_STATUS -> polling when ["!=", (object/get ?row envelopeId), ""]
376
+ (set @entity.id ?id)
377
+ (call-service esign getEnvelopeStatus {
378
+ envelopeId: (object/get ?row envelopeId)
379
+ } {
380
+ emit: { failure: ENVELOPE_STATUS_FAILED, success: ENVELOPE_STATUS_LOADED }
381
+ })
382
+ CHECK_ENVELOPE_STATUS -> idle
383
+ (notify info "This request was never dispatched to the e-sign provider — send it first")
384
+ ENVELOPE_STAMPED -> idle
385
+ ENVELOPE_STAMP_FAILED -> idle
386
+ (notify warning "Could not record the envelope state on the request")
387
+ }
388
+
389
+ state dispatching {
390
+ ENVELOPE_SENT -> idle
391
+ (persist update SignatureSession {
392
+ id: @entity.id
393
+ envelopeId: ?envelopeId
394
+ sentAt: @now
395
+ status: sent
396
+ } {
397
+ emit: { failure: ENVELOPE_STAMP_FAILED, success: ENVELOPE_STAMPED }
398
+ })
399
+ (notify success "Signature request sent")
400
+ ENVELOPE_SEND_FAILED -> idle
401
+ (notify warning "Could not send the signature request")
402
+ }
403
+
404
+ state polling {
405
+ ENVELOPE_STATUS_LOADED -> idle when ["=", ?status, "completed"]
406
+ (persist update SignatureSession {
407
+ id: @entity.id
408
+ signedAt: @now
409
+ status: signed
410
+ } {
411
+ emit: { failure: ENVELOPE_STAMP_FAILED, success: ENVELOPE_STAMPED }
412
+ })
413
+ ENVELOPE_STATUS_LOADED -> idle when ["=", ?status, "delivered"]
414
+ (persist update SignatureSession {
415
+ id: @entity.id
416
+ status: viewed
417
+ } {
418
+ emit: { failure: ENVELOPE_STAMP_FAILED, success: ENVELOPE_STAMPED }
419
+ })
420
+ ENVELOPE_STATUS_LOADED -> idle when ["=", ?status, "declined"]
421
+ (persist update SignatureSession {
422
+ id: @entity.id
423
+ status: declined
424
+ } {
425
+ emit: { failure: ENVELOPE_STAMP_FAILED, success: ENVELOPE_STAMPED }
426
+ })
427
+ ENVELOPE_STATUS_LOADED -> idle
428
+ (persist update SignatureSession {
429
+ id: @entity.id
430
+ status: sent
431
+ } {
432
+ emit: { failure: ENVELOPE_STAMP_FAILED, success: ENVELOPE_STAMPED }
433
+ })
434
+ ENVELOPE_STATUS_FAILED -> idle
435
+ (notify warning "Could not read the envelope status")
436
+ }
437
+
438
+ emits {
439
+ ENVELOPE_SENT {
440
+ envelopeId : string
441
+ status : string
442
+ }
443
+ @description "The provider accepted the envelope; carries its id."
444
+ @tier "internal"
445
+ ENVELOPE_SEND_FAILED {
446
+ error : string
447
+ code : string
448
+ }
449
+ @description "The e-sign provider rejected or failed the dispatch."
450
+ @tier "internal"
451
+ ENVELOPE_STATUS_LOADED {
452
+ status : string
453
+ completedAt : string
454
+ }
455
+ @description "The provider returned the envelope's current status."
456
+ @tier "internal"
457
+ ENVELOPE_STATUS_FAILED {
458
+ error : string
459
+ code : string
460
+ }
461
+ @description "Reading the envelope status failed."
462
+ @tier "internal"
463
+ ENVELOPE_STAMPED -> external {
464
+ id : string
465
+ }
466
+ @description "The session row now carries the latest provider state (envelopeId / status / timestamps)."
467
+ @tier "domain"
468
+ ENVELOPE_STAMP_FAILED {
469
+ error : string
470
+ code : string
471
+ }
472
+ @description "Writing the provider state back onto the session row failed."
473
+ @tier "internal"
474
+ }
475
+
476
+ listens {
477
+ SEND_ENVELOPE {
478
+ id : string!
479
+ row : SignatureSession
480
+ file : { name : string, size : number, type : string, content : string }
481
+ }
482
+ @description "Dispatch this session as a real e-sign envelope; `file.content` is the document as a base64 data URL (UploadDropZone shape)."
483
+ @synonyms "send envelope, dispatch signature, send for signing"
484
+ @tier "domain"
485
+ SignatureSessionBrowse.CHECK_ENVELOPE_STATUS -> CHECK_ENVELOPE_STATUS
486
+ CHECK_ENVELOPE_STATUS {
487
+ id : string!
488
+ row : SignatureSession
489
+ }
490
+ @description "Pull the provider's current envelope status and stamp it onto the session row."
491
+ @synonyms "refresh status, check signature, poll envelope"
492
+ @tier "domain"
493
+ ENVELOPE_SENT {
494
+ envelopeId : string
495
+ status : string
496
+ }
497
+ ENVELOPE_SEND_FAILED {
498
+ error : string
499
+ code : string
500
+ }
501
+ ENVELOPE_STATUS_LOADED {
502
+ status : string
503
+ completedAt : string
504
+ }
505
+ ENVELOPE_STATUS_FAILED {
506
+ error : string
507
+ code : string
508
+ }
509
+ ENVELOPE_STAMPED {
510
+ id : string
511
+ }
512
+ ENVELOPE_STAMP_FAILED {
513
+ error : string
514
+ code : string
515
+ }
516
+ }
517
+ }
518
+
519
+ page "/esign-flow" as SignatureSessionPage -> SignatureSessionBrowse, SignatureSessionCreate, SignatureSessionDelete, SignatureSessionPersistor, EnvelopeDispatcher
343
520
  }
@@ -32,6 +32,7 @@ orbital CalendarGridOrbital {
32
32
  dayWindow: @config.dayWindow
33
33
  titleField: @config.titleField
34
34
  startField: @config.startField
35
+ endField: @config.endField
35
36
  colorField: @config.colorField
36
37
  type: calendar-grid
37
38
  })
@@ -51,6 +52,7 @@ orbital CalendarGridOrbital {
51
52
  dayWindow: @config.dayWindow
52
53
  titleField: @config.titleField
53
54
  startField: @config.startField
55
+ endField: @config.endField
54
56
  colorField: @config.colorField
55
57
  type: calendar-grid
56
58
  })
@@ -148,6 +150,10 @@ orbital CalendarGridOrbital {
148
150
  @label "Start Field"
149
151
  @description "Row field holding the start timestamp (ISO or epoch). Defaults to `startTime`."
150
152
  @tier "presentation"
153
+ endField : string = endTime
154
+ @label "End Field"
155
+ @description "Row field holding the end timestamp (ISO or epoch). Defaults to `endTime`. When a row carries an end, the event's chip renders in its start slot and every further slot it covers shows a subdued continuation bar — without this, multi-hour events silently collapsed to their first hour."
156
+ @tier "presentation"
151
157
  colorField : string = color
152
158
  @label "Color Field"
153
159
  @description "Row field holding the chip's Tailwind colour class. Defaults to `color`."