support_desk 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +206 -0
- data/README.md +227 -16
- data/app/controllers/support_desk/application_controller.rb +5 -2
- data/app/controllers/support_desk/tickets_controller.rb +68 -3
- data/app/helpers/support_desk/engine_helper.rb +10 -10
- data/app/helpers/support_desk/thread_helper.rb +29 -0
- data/app/views/support_desk/tickets/show.html.erb +8 -0
- data/config/locales/support_desk.en.yml +28 -0
- data/config/locales/support_desk.es.yml +28 -0
- data/lib/generators/support_desk/install_generator.rb +9 -3
- data/lib/generators/support_desk/templates/create_support_desk_message_registrations.rb.erb +42 -0
- data/lib/generators/support_desk/templates/initializer.rb +21 -1
- data/lib/generators/support_desk/upgrade_generator.rb +15 -7
- data/lib/support_desk/brief.rb +1 -1
- data/lib/support_desk/configuration.rb +100 -19
- data/lib/support_desk/conversation_access.rb +42 -0
- data/lib/support_desk/doctor.rb +30 -3
- data/lib/support_desk/engine.rb +13 -1
- data/lib/support_desk/message_signatures.rb +20 -0
- data/lib/support_desk/models/assistant.rb +43 -8
- data/lib/support_desk/models/draft.rb +9 -0
- data/lib/support_desk/models/message_registration.rb +12 -0
- data/lib/support_desk/models/ticket/assistance.rb +254 -30
- data/lib/support_desk/models/ticket.rb +172 -67
- data/lib/support_desk/transcript.rb +9 -12
- data/lib/support_desk/version.rb +1 -1
- data/lib/support_desk/wizard.rb +1 -5
- data/lib/support_desk.rb +82 -11
- data/lib/tasks/support_desk.rake +12 -1
- metadata +20 -11
data/lib/support_desk.rb
CHANGED
|
@@ -124,7 +124,11 @@ module SupportDesk
|
|
|
124
124
|
"`config.assistant #{key.inspect} do |assistant| … end`"
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
# Resolving her is also when what she is CALLED is written down, so a
|
|
128
|
+
# message she signed still says the same thing after somebody takes
|
|
129
|
+
# her out of the initializer (R8). It writes only when configuration
|
|
130
|
+
# and the row disagree.
|
|
131
|
+
(assistants[key] ||= Assistant.for(key)).snapshot_disclosure!
|
|
128
132
|
end
|
|
129
133
|
|
|
130
134
|
# Every assistant record this process has resolved, keyed by key.
|
|
@@ -146,6 +150,31 @@ module SupportDesk
|
|
|
146
150
|
record.respond_to?(:support_agent_kind) && record.support_agent_kind == :ai
|
|
147
151
|
end
|
|
148
152
|
|
|
153
|
+
# Give back every seat an assistant can no longer sit in — she was
|
|
154
|
+
# switched off, her configuration was removed, or her policy no longer
|
|
155
|
+
# lets her hold a case — and ask for a person on each of them.
|
|
156
|
+
#
|
|
157
|
+
# Invalid-assignee recovery, NOT silence detection: it reads the seats
|
|
158
|
+
# that exist rather than the assistants a running process happens to
|
|
159
|
+
# have configured, so a `deactivate!`, a flag flipped off and a topic
|
|
160
|
+
# cap tightened are all covered, with no `responds_within` and no
|
|
161
|
+
# overdue clock anywhere in it (R6). `release_silent_assistants!` runs
|
|
162
|
+
# it first; `rake support_desk:reclaim_assistant_seats` runs it alone.
|
|
163
|
+
#
|
|
164
|
+
# Returns how many seats it reclaimed.
|
|
165
|
+
def reclaim_assistant_seats!
|
|
166
|
+
return 0 unless Assistant.table_exists?
|
|
167
|
+
|
|
168
|
+
reclaimed = 0
|
|
169
|
+
Ticket.open.held_by_assistants.find_each do |ticket|
|
|
170
|
+
reclaimed += 1 if ticket.reclaim_assistant_seat!
|
|
171
|
+
rescue StandardError => e
|
|
172
|
+
report_error(e, context: { hook: :reclaim_assistant_seats, ticket: ticket.id })
|
|
173
|
+
end
|
|
174
|
+
logger&.info("[support_desk] reclaimed #{reclaimed} stranded assistant seat(s)") if reclaimed.positive?
|
|
175
|
+
reclaimed
|
|
176
|
+
end
|
|
177
|
+
|
|
149
178
|
# Release every assistant who has sat on a case longer than her
|
|
150
179
|
# `responds_within` without answering — and ask for a person on it.
|
|
151
180
|
#
|
|
@@ -154,18 +183,19 @@ module SupportDesk
|
|
|
154
183
|
# Run it every minute (`rake support_desk:release_silent_assistants`).
|
|
155
184
|
# Returns how many cases it moved.
|
|
156
185
|
def release_silent_assistants!
|
|
157
|
-
|
|
186
|
+
# A seat nobody can sit in any more is not a silence problem, and it
|
|
187
|
+
# must not need a `responds_within` to be noticed (R6).
|
|
188
|
+
moved = reclaim_assistant_seats!
|
|
158
189
|
config.assistants.each_key do |key|
|
|
159
190
|
agent = assistant(key)
|
|
160
191
|
window = agent&.responds_within
|
|
161
192
|
next if window.nil?
|
|
162
193
|
|
|
163
194
|
Ticket.open.assigned_to(agent).awaiting_reply.waiting_over(window).find_each do |ticket|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
moved += 1 if ticket.human_required?
|
|
195
|
+
# Conditional, under the case's own lock: every predicate above was
|
|
196
|
+
# true when this row was SELECTED, and a person may have answered
|
|
197
|
+
# it since (R5).
|
|
198
|
+
moved += 1 if ticket.escalate_if_still_silent!(agent, window)
|
|
169
199
|
rescue StandardError => e
|
|
170
200
|
report_error(e, context: { hook: :release_silent_assistants, ticket: ticket.id })
|
|
171
201
|
end
|
|
@@ -186,6 +216,19 @@ module SupportDesk
|
|
|
186
216
|
|
|
187
217
|
config.desks.each_key do |key|
|
|
188
218
|
agent = desk(key)&.assistant
|
|
219
|
+
# Repair before deciding. A requester message whose registration was
|
|
220
|
+
# lost after commit leaves the clocks describing a case that no longer
|
|
221
|
+
# exists — and `assistant_idle_since` reads those clocks, so the case
|
|
222
|
+
# it most needs to find is the one it cannot see. Folding the message
|
|
223
|
+
# in COMMITS on its own and emits the turn it produces, so a dead
|
|
224
|
+
# process followed by nothing but this task still ends in an
|
|
225
|
+
# actionable turn (R3).
|
|
226
|
+
Ticket.for_desk(key).with_unregistered_requester_messages.find_each do |ticket|
|
|
227
|
+
emitted += 1 if ticket.reconcile_and_commit!.positive?
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
report_error(e, context: { hook: :redispatch_assistant_turns, ticket: ticket.id })
|
|
230
|
+
end
|
|
231
|
+
|
|
189
232
|
next if agent.nil?
|
|
190
233
|
|
|
191
234
|
Ticket.open.for_desk(key).assistant_idle_since(older_than.ago).find_each do |ticket|
|
|
@@ -359,8 +402,10 @@ module SupportDesk
|
|
|
359
402
|
#
|
|
360
403
|
# Both surfaces are ordinary PUSHED screens (`context: "default"`), never
|
|
361
404
|
# modals: the wizard is three real URLs and a modal would break the back
|
|
362
|
-
# gesture between them. The thread itself is
|
|
363
|
-
# a chats conversation, and it stays under
|
|
405
|
+
# gesture between them. The thread itself is absent under the default
|
|
406
|
+
# `thread_page = :chats` — it is a chats conversation, and it stays under
|
|
407
|
+
# the host's chats rule. Under `:support` it is this engine's ticket page,
|
|
408
|
+
# and a third rule pushes it like the list.
|
|
364
409
|
#
|
|
365
410
|
# +mount+ defaults to wherever the engine is mounted and +title+ to the
|
|
366
411
|
# desk's name.
|
|
@@ -386,8 +431,16 @@ module SupportDesk
|
|
|
386
431
|
properties: { context: "default", title: title, pull_to_refresh_enabled: false },
|
|
387
432
|
comment: "The wizard: every step is a real URL, so it pushes and the back gesture works. " \
|
|
388
433
|
"Pull to refresh is off — it would throw away what the requester has typed."
|
|
389
|
-
}
|
|
390
|
-
|
|
434
|
+
},
|
|
435
|
+
(if config.support_thread_page?
|
|
436
|
+
{
|
|
437
|
+
patterns: [ "^#{prefix}/tickets/[^/?]+(?:\\?.*)?$" ],
|
|
438
|
+
properties: { context: "default", title: title, pull_to_refresh_enabled: false },
|
|
439
|
+
comment: "A case, read on the support page (thread_page = :support). Pull to refresh is off " \
|
|
440
|
+
"for the same reason as the wizard: the composer at its foot holds what is typed."
|
|
441
|
+
}
|
|
442
|
+
end)
|
|
443
|
+
].compact
|
|
391
444
|
end
|
|
392
445
|
|
|
393
446
|
# A duration in the reader's own language ("1 día", "4 hours") — the
|
|
@@ -406,6 +459,24 @@ module SupportDesk
|
|
|
406
459
|
words.start_with?("Translation missing") ? duration.inspect : words
|
|
407
460
|
end
|
|
408
461
|
|
|
462
|
+
# The answer promise, in the unit the desk made it in: `reply_within =
|
|
463
|
+
# 72.hours` reads "72 hours", `3.days` reads "3 days", `24.hours` reads
|
|
464
|
+
# "24 hours". A wait is approximate and `humanize_duration` rounds it
|
|
465
|
+
# ("about 8 hours", "3 days"); a promise is a number somebody chose, and
|
|
466
|
+
# rounding it says something they did not.
|
|
467
|
+
#
|
|
468
|
+
# The words are `support_desk.durations.<unit>`, pluralized by count. A
|
|
469
|
+
# duration of more than one unit (1.day + 2.hours), a fractional one, or
|
|
470
|
+
# a unit with no words in this locale is said the way a wait is.
|
|
471
|
+
def humanize_promise(duration)
|
|
472
|
+
parts = duration.respond_to?(:parts) ? duration.parts : {}
|
|
473
|
+
unit, count = parts.first if parts.size == 1
|
|
474
|
+
key = :"support_desk.durations.#{unit}"
|
|
475
|
+
return humanize_duration(duration) unless count.is_a?(Integer) && I18n.exists?(key)
|
|
476
|
+
|
|
477
|
+
I18n.t(key, count: count)
|
|
478
|
+
end
|
|
479
|
+
|
|
409
480
|
# A stable, URL-safe key for an actor (agent, requester, desk), used in
|
|
410
481
|
# cache keys and event payloads. GlobalID params already encode class +
|
|
411
482
|
# id, so two classes can never collide.
|
data/lib/tasks/support_desk.rake
CHANGED
|
@@ -38,6 +38,17 @@ namespace :support_desk do
|
|
|
38
38
|
puts "[support_desk] #{moved} case(s) handed to a person."
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
desc "Give back every seat an assistant can no longer sit in (run every minute)"
|
|
42
|
+
task reclaim_assistant_seats: :environment do
|
|
43
|
+
# Invalid-assignee recovery: she was switched off, her configuration was
|
|
44
|
+
# taken away, or her policy no longer lets her hold a case. It needs no
|
|
45
|
+
# `responds_within` and no overdue clock — `release_silent_assistants`
|
|
46
|
+
# runs it first, and this is the same work on its own.
|
|
47
|
+
reclaimed = SupportDesk.reclaim_assistant_seats!
|
|
48
|
+
|
|
49
|
+
puts "[support_desk] #{reclaimed} stranded seat(s) given back to people."
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
desc "Re-emit the turn for cases nobody answered (OLDER_THAN=60 seconds; run every 5 minutes)"
|
|
42
53
|
task redispatch_assistant_turns: :environment do
|
|
43
54
|
# At-least-once, and that is safe: the turn is consumed by the first
|
|
@@ -48,7 +59,7 @@ namespace :support_desk do
|
|
|
48
59
|
puts "[support_desk] #{emitted} turn(s) re-emitted (idle for more than #{older_than.inspect})."
|
|
49
60
|
end
|
|
50
61
|
|
|
51
|
-
desc "
|
|
62
|
+
desc "Report assistant status without changing tickets"
|
|
52
63
|
task assistant_status: :environment do
|
|
53
64
|
if SupportDesk.config.assistants.empty?
|
|
54
65
|
puts "[support_desk] no assistant is configured."
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: support_desk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-18 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: activerecord
|
|
@@ -53,22 +54,22 @@ dependencies:
|
|
|
53
54
|
name: chats
|
|
54
55
|
requirement: !ruby/object:Gem::Requirement
|
|
55
56
|
requirements:
|
|
56
|
-
- - "~>"
|
|
57
|
-
- !ruby/object:Gem::Version
|
|
58
|
-
version: '0.3'
|
|
59
57
|
- - ">="
|
|
60
58
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
59
|
+
version: 0.5.1
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '1.0'
|
|
62
63
|
type: :runtime
|
|
63
64
|
prerelease: false
|
|
64
65
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
66
|
requirements:
|
|
66
|
-
- - "~>"
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0.3'
|
|
69
67
|
- - ">="
|
|
70
68
|
- !ruby/object:Gem::Version
|
|
71
|
-
version: 0.
|
|
69
|
+
version: 0.5.1
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '1.0'
|
|
72
73
|
- !ruby/object:Gem::Dependency
|
|
73
74
|
name: globalid
|
|
74
75
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -143,6 +144,7 @@ files:
|
|
|
143
144
|
- app/controllers/support_desk/console/tickets_controller.rb
|
|
144
145
|
- app/controllers/support_desk/tickets_controller.rb
|
|
145
146
|
- app/helpers/support_desk/engine_helper.rb
|
|
147
|
+
- app/helpers/support_desk/thread_helper.rb
|
|
146
148
|
- app/views/chats/slots/_inbox_top.html.erb
|
|
147
149
|
- app/views/chats/slots/_locked_composer.html.erb
|
|
148
150
|
- app/views/support_desk/console/tickets/_actions.html.erb
|
|
@@ -171,6 +173,7 @@ files:
|
|
|
171
173
|
- app/views/support_desk/tickets/index.html.erb
|
|
172
174
|
- app/views/support_desk/tickets/new.html.erb
|
|
173
175
|
- app/views/support_desk/tickets/rate_limited.html.erb
|
|
176
|
+
- app/views/support_desk/tickets/show.html.erb
|
|
174
177
|
- config/console_routes.rb
|
|
175
178
|
- config/locales/support_desk.console.en.yml
|
|
176
179
|
- config/locales/support_desk.console.es.yml
|
|
@@ -191,6 +194,7 @@ files:
|
|
|
191
194
|
- lib/generators/support_desk/templates/assistant/turn_job_test.rb.erb
|
|
192
195
|
- lib/generators/support_desk/templates/console/controller.rb.erb
|
|
193
196
|
- lib/generators/support_desk/templates/console/resource.rb.erb
|
|
197
|
+
- lib/generators/support_desk/templates/create_support_desk_message_registrations.rb.erb
|
|
194
198
|
- lib/generators/support_desk/templates/create_support_desk_tables.rb.erb
|
|
195
199
|
- lib/generators/support_desk/templates/initializer.rb
|
|
196
200
|
- lib/generators/support_desk/upgrade_generator.rb
|
|
@@ -203,12 +207,14 @@ files:
|
|
|
203
207
|
- lib/support_desk/console_engine.rb
|
|
204
208
|
- lib/support_desk/console_routes.rb
|
|
205
209
|
- lib/support_desk/context_card.rb
|
|
210
|
+
- lib/support_desk/conversation_access.rb
|
|
206
211
|
- lib/support_desk/current.rb
|
|
207
212
|
- lib/support_desk/doctor.rb
|
|
208
213
|
- lib/support_desk/engine.rb
|
|
209
214
|
- lib/support_desk/errors.rb
|
|
210
215
|
- lib/support_desk/events.rb
|
|
211
216
|
- lib/support_desk/macros.rb
|
|
217
|
+
- lib/support_desk/message_signatures.rb
|
|
212
218
|
- lib/support_desk/models/application_record.rb
|
|
213
219
|
- lib/support_desk/models/assignment.rb
|
|
214
220
|
- lib/support_desk/models/assistant.rb
|
|
@@ -218,6 +224,7 @@ files:
|
|
|
218
224
|
- lib/support_desk/models/desk.rb
|
|
219
225
|
- lib/support_desk/models/draft.rb
|
|
220
226
|
- lib/support_desk/models/event.rb
|
|
227
|
+
- lib/support_desk/models/message_registration.rb
|
|
221
228
|
- lib/support_desk/models/ticket.rb
|
|
222
229
|
- lib/support_desk/models/ticket/assistance.rb
|
|
223
230
|
- lib/support_desk/outcome.rb
|
|
@@ -241,6 +248,7 @@ metadata:
|
|
|
241
248
|
bug_tracker_uri: https://github.com/rameerez/support_desk/issues
|
|
242
249
|
documentation_uri: https://github.com/rameerez/support_desk#readme
|
|
243
250
|
rubygems_mfa_required: 'true'
|
|
251
|
+
post_install_message:
|
|
244
252
|
rdoc_options: []
|
|
245
253
|
require_paths:
|
|
246
254
|
- lib
|
|
@@ -255,7 +263,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
255
263
|
- !ruby/object:Gem::Version
|
|
256
264
|
version: '0'
|
|
257
265
|
requirements: []
|
|
258
|
-
rubygems_version: 3.
|
|
266
|
+
rubygems_version: 3.4.10
|
|
267
|
+
signing_key:
|
|
259
268
|
specification_version: 4
|
|
260
269
|
summary: 'Customer support for your Rails app: tickets that are conversations, worked
|
|
261
270
|
by your team and by AI agents with real guardrails'
|