solid_objects 0.12.0 → 0.13.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -0
  3. data/README.md +72 -6
  4. data/app/models/solid_objects/broadcast.rb +8 -0
  5. data/docs/adr/0009-realtime-updates.md +5 -1
  6. data/docs/architecture.md +4 -2
  7. data/docs/authorization.md +11 -4
  8. data/docs/correctness.md +3 -3
  9. data/docs/dashboard.md +200 -0
  10. data/docs/database-schema.md +5 -4
  11. data/docs/development.md +12 -0
  12. data/docs/realtime.md +37 -7
  13. data/docs/roadmap.md +28 -6
  14. data/docs/security.md +8 -0
  15. data/examples/application/README.md +5 -5
  16. data/examples/application/app/actors/chat_room_actor.rb +1 -1
  17. data/examples/application/app/actors/shopping_cart_actor.rb +5 -5
  18. data/lib/solid_objects/actor.rb +6 -5
  19. data/lib/solid_objects/actor_channel.rb +7 -4
  20. data/lib/solid_objects/actor_definition.rb +15 -3
  21. data/lib/solid_objects/actor_view.rb +6 -1
  22. data/lib/solid_objects/effect_executor.rb +10 -2
  23. data/lib/solid_objects/errors.rb +3 -0
  24. data/lib/solid_objects/executor.rb +7 -1
  25. data/lib/solid_objects/reminder_scheduler.rb +28 -14
  26. data/lib/solid_objects/test_helper.rb +16 -0
  27. data/lib/solid_objects/turbo_stream_renderer.rb +3 -3
  28. data/lib/solid_objects/version.rb +1 -1
  29. data/lib/solid_objects/web/action.rb +109 -0
  30. data/lib/solid_objects/web/application.rb +239 -0
  31. data/lib/solid_objects/web/csrf_protection.rb +130 -0
  32. data/lib/solid_objects/web/helpers.rb +227 -0
  33. data/lib/solid_objects/web/paginator.rb +64 -0
  34. data/lib/solid_objects/web/route.rb +55 -0
  35. data/lib/solid_objects/web/router.rb +46 -0
  36. data/lib/solid_objects/web/statistics.rb +78 -0
  37. data/lib/solid_objects/web.rb +222 -0
  38. data/sig/generated/lib/solid_objects/actor.rbs +2 -2
  39. data/sig/generated/lib/solid_objects/actor_definition.rbs +9 -2
  40. data/sig/generated/lib/solid_objects/errors.rbs +3 -0
  41. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +9 -6
  42. data/sig/generated/lib/solid_objects/test_helper.rbs +3 -0
  43. data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
  44. data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
  45. data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
  46. data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
  47. data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
  48. data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
  49. data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
  50. data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
  51. data/sig/generated/lib/solid_objects/web.rbs +129 -0
  52. data/sig/generated/models/solid_objects/broadcast.rbs +2 -0
  53. data/web/assets/javascripts/application.js +118 -0
  54. data/web/assets/javascripts/charts.js +190 -0
  55. data/web/assets/stylesheets/application.css +527 -0
  56. data/web/views/_messages.erb +29 -0
  57. data/web/views/_navigation.erb +15 -0
  58. data/web/views/_paging.erb +17 -0
  59. data/web/views/_status_filter.erb +8 -0
  60. data/web/views/_summary.erb +39 -0
  61. data/web/views/broadcasts.erb +35 -0
  62. data/web/views/dashboard.erb +128 -0
  63. data/web/views/dead_letter.erb +40 -0
  64. data/web/views/dead_letters.erb +42 -0
  65. data/web/views/effects.erb +35 -0
  66. data/web/views/instance.erb +139 -0
  67. data/web/views/instances.erb +49 -0
  68. data/web/views/layout.erb +22 -0
  69. data/web/views/mailbox.erb +35 -0
  70. data/web/views/message.erb +34 -0
  71. data/web/views/processes.erb +37 -0
  72. data/web/views/reminders.erb +37 -0
  73. metadata +55 -2
@@ -0,0 +1,29 @@
1
+ <h3><%= h(locals.fetch(:title)) %></h3>
2
+ <% if locals.fetch(:messages).empty? %>
3
+ <p class="empty"><%= h(locals.fetch(:empty, "None.")) %></p>
4
+ <% else %>
5
+ <table>
6
+ <thead>
7
+ <tr>
8
+ <th>Sequence</th>
9
+ <th>Operation</th>
10
+ <th>Delivery</th>
11
+ <th>Attempts</th>
12
+ <th>Available</th>
13
+ <th>Completed</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% locals.fetch(:messages).each do |message| %>
18
+ <tr>
19
+ <td><a href="<%= h(path_to("/messages/#{message.id}")) %>"><%= number(message.sequence) %></a></td>
20
+ <td><%= h(message.operation) %></td>
21
+ <td><%= status_label(message.delivery_mode) %></td>
22
+ <td><%= number(message.attempt_count) %> / <%= number(message.max_attempts) %></td>
23
+ <td><%= relative_time(message.available_at) %></td>
24
+ <td><%= relative_time(message.completed_at) %></td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <header class="bar">
2
+ <a class="brand" href="<%= h(path_to("/")) %>">
3
+ <span class="brand-mark">SO</span>
4
+ <span class="brand-name">Solid Objects</span>
5
+ </a>
6
+ <nav class="tabs">
7
+ <% tabs.each do |label, path| %>
8
+ <a class="tab<%= current_tab?(path) ? " tab-current" : "" %>" href="<%= h(path_to(path)) %>"><%= h(label) %></a>
9
+ <% end %>
10
+ </nav>
11
+ <div class="bar-right">
12
+ <span class="environment"><%= h(environment_name) %></span>
13
+ <button type="button" class="poll-toggle" data-poll-toggle aria-pressed="false">Live</button>
14
+ </div>
15
+ </header>
@@ -0,0 +1,17 @@
1
+ <nav class="paging" aria-label="Pages">
2
+ <span class="paging-range">
3
+ <% if @paginator.total.zero? %>
4
+ No rows
5
+ <% else %>
6
+ <%= number(@paginator.first_record) %>&ndash;<%= number(@paginator.last_record) %> of <%= number(@paginator.total) %>
7
+ <% end %>
8
+ </span>
9
+ <span class="paging-links">
10
+ <% if @paginator.previous_page %>
11
+ <a href="<%= page_link("page" => @paginator.previous_page) %>">Previous</a>
12
+ <% end %>
13
+ <% if @paginator.next_page %>
14
+ <a href="<%= page_link("page" => @paginator.next_page) %>">Next</a>
15
+ <% end %>
16
+ </span>
17
+ </nav>
@@ -0,0 +1,8 @@
1
+ <nav class="filters" aria-label="Status filter">
2
+ <% if locals.fetch(:all, true) %>
3
+ <a class="filter<%= locals[:selected].nil? ? " filter-current" : "" %>" href="<%= page_link("status" => nil, "page" => nil) %>">All</a>
4
+ <% end %>
5
+ <% locals.fetch(:statuses).each do |status| %>
6
+ <a class="filter<%= (locals[:selected] == status) ? " filter-current" : "" %>" href="<%= page_link("status" => status, "page" => nil) %>"><%= h(status) %></a>
7
+ <% end %>
8
+ </nav>
@@ -0,0 +1,39 @@
1
+ <% summary = statistics.to_h %>
2
+ <section class="summary" aria-label="Runtime totals">
3
+ <a class="metric" href="<%= h(path_to("/instances")) %>">
4
+ <span class="metric-label">Instances</span>
5
+ <span class="metric-value" data-statistic="instances.total"><%= number(summary.dig(:instances, :total)) %></span>
6
+ </a>
7
+ <a class="metric" href="<%= h(path_to("/mailbox")) %>">
8
+ <span class="metric-label">Ready</span>
9
+ <span class="metric-value" data-statistic="mailbox.ready"><%= number(summary.dig(:mailbox, :ready)) %></span>
10
+ </a>
11
+ <a class="metric" href="<%= h(path_to("/mailbox?status=claimed")) %>">
12
+ <span class="metric-label">Claimed</span>
13
+ <span class="metric-value" data-statistic="mailbox.claimed"><%= number(summary.dig(:mailbox, :claimed)) %></span>
14
+ </a>
15
+ <div class="metric">
16
+ <span class="metric-label">Mailbox lag</span>
17
+ <span class="metric-value" data-statistic="mailbox.latency" data-format="duration"><%= duration(summary.dig(:mailbox, :latency)) %></span>
18
+ </div>
19
+ <a class="metric" href="<%= h(path_to("/effects?status=pending")) %>">
20
+ <span class="metric-label">Effects</span>
21
+ <span class="metric-value" data-statistic="effects.pending"><%= number(summary.dig(:effects, :pending)) %></span>
22
+ </a>
23
+ <a class="metric" href="<%= h(path_to("/broadcasts?status=pending")) %>">
24
+ <span class="metric-label">Broadcasts</span>
25
+ <span class="metric-value" data-statistic="broadcasts.pending"><%= number(summary.dig(:broadcasts, :pending)) %></span>
26
+ </a>
27
+ <a class="metric" href="<%= h(path_to("/reminders?status=scheduled")) %>">
28
+ <span class="metric-label">Reminders due</span>
29
+ <span class="metric-value" data-statistic="reminders.due"><%= number(summary.dig(:reminders, :due)) %></span>
30
+ </a>
31
+ <a class="metric metric-alert" href="<%= h(path_to("/dead_letters")) %>">
32
+ <span class="metric-label">Dead letters</span>
33
+ <span class="metric-value" data-statistic="dead_letters.total"><%= number(summary.dig(:dead_letters, :total)) %></span>
34
+ </a>
35
+ <a class="metric" href="<%= h(path_to("/processes?status=running")) %>">
36
+ <span class="metric-label">Processes</span>
37
+ <span class="metric-value" data-statistic="processes.running"><%= number(summary.dig(:processes, :running)) %></span>
38
+ </a>
39
+ </section>
@@ -0,0 +1,35 @@
1
+ <section class="panel">
2
+ <h2>Broadcasts</h2>
3
+ <%= erb(:_status_filter, statuses: SolidObjects::Web::Statistics::BROADCAST_STATUSES, selected: @status) %>
4
+
5
+ <% if @paginator.records.empty? %>
6
+ <p class="empty">No broadcast matches this filter.</p>
7
+ <% else %>
8
+ <table>
9
+ <thead>
10
+ <tr>
11
+ <th>Observable</th>
12
+ <th>Actor</th>
13
+ <th>Status</th>
14
+ <th>State version</th>
15
+ <th>Attempts</th>
16
+ <th>Delivered</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @paginator.records.each do |broadcast| %>
21
+ <tr>
22
+ <td><%= h(broadcast.observable_name) %></td>
23
+ <td><a href="<%= h(path_to("/instances/#{broadcast.instance_id}")) %>">instance <%= number(broadcast.instance_id) %></a></td>
24
+ <td><%= status_label(broadcast.status) %></td>
25
+ <td><%= number(broadcast.state_version) %></td>
26
+ <td><%= number(broadcast.attempt_count) %></td>
27
+ <td><%= relative_time(broadcast.delivered_at) %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ <% end %>
33
+
34
+ <%= erb(:_paging) %>
35
+ </section>
@@ -0,0 +1,128 @@
1
+ <% if SolidObjects::Web.charts? %>
2
+ <section class="charts">
3
+ <article class="panel chart-panel">
4
+ <h2>Instances by actor type</h2>
5
+ <%= chart("instances_by_type", @instances_by_type) %>
6
+ </article>
7
+ <article class="panel chart-panel">
8
+ <h2>Mailbox</h2>
9
+ <%= chart("mailbox_depth", {
10
+ "Ready" => @statistics.dig(:mailbox, :ready),
11
+ "Due" => @statistics.dig(:mailbox, :due),
12
+ "Claimed" => @statistics.dig(:mailbox, :claimed)
13
+ }) %>
14
+ </article>
15
+ <article class="panel chart-panel chart-wide">
16
+ <h2>Outboxes and reminders by status</h2>
17
+ <%= chart("work_by_status", {
18
+ "Effects" => @statistics.fetch(:effects),
19
+ "Broadcasts" => @statistics.fetch(:broadcasts),
20
+ "Reminders" => @statistics.fetch(:reminders).except(:due)
21
+ }) %>
22
+ </article>
23
+ </section>
24
+ <% end %>
25
+
26
+ <section class="cards">
27
+ <article class="card">
28
+ <h2>Instances</h2>
29
+ <dl>
30
+ <div><dt>Total</dt><dd><%= number(@statistics.dig(:instances, :total)) %></dd></div>
31
+ <div><dt>Activated</dt><dd><%= number(@statistics.dig(:instances, :activated)) %></dd></div>
32
+ <div><dt>Paused</dt><dd><%= number(@statistics.dig(:instances, :paused)) %></dd></div>
33
+ </dl>
34
+ </article>
35
+
36
+ <article class="card">
37
+ <h2>Mailbox</h2>
38
+ <dl>
39
+ <div><dt>Ready</dt><dd><%= number(@statistics.dig(:mailbox, :ready)) %></dd></div>
40
+ <div><dt>Due</dt><dd><%= number(@statistics.dig(:mailbox, :due)) %></dd></div>
41
+ <div><dt>Claimed</dt><dd><%= number(@statistics.dig(:mailbox, :claimed)) %></dd></div>
42
+ <div><dt>Oldest due</dt><dd><%= duration(@statistics.dig(:mailbox, :latency)) %></dd></div>
43
+ </dl>
44
+ </article>
45
+
46
+ <article class="card">
47
+ <h2>Effects</h2>
48
+ <dl>
49
+ <% @statistics.fetch(:effects).each do |status, count| %>
50
+ <div><dt><%= h(status) %></dt><dd><%= number(count) %></dd></div>
51
+ <% end %>
52
+ </dl>
53
+ </article>
54
+
55
+ <article class="card">
56
+ <h2>Broadcasts</h2>
57
+ <dl>
58
+ <% @statistics.fetch(:broadcasts).each do |status, count| %>
59
+ <div><dt><%= h(status) %></dt><dd><%= number(count) %></dd></div>
60
+ <% end %>
61
+ </dl>
62
+ </article>
63
+
64
+ <article class="card">
65
+ <h2>Reminders</h2>
66
+ <dl>
67
+ <% @statistics.fetch(:reminders).each do |status, count| %>
68
+ <div><dt><%= h(status) %></dt><dd><%= number(count) %></dd></div>
69
+ <% end %>
70
+ </dl>
71
+ </article>
72
+
73
+ <article class="card">
74
+ <h2>Processes</h2>
75
+ <dl>
76
+ <% @statistics.fetch(:processes).each do |state, count| %>
77
+ <div><dt><%= h(state) %></dt><dd><%= number(count) %></dd></div>
78
+ <% end %>
79
+ </dl>
80
+ </article>
81
+ </section>
82
+
83
+ <section class="panel">
84
+ <h2>Processes</h2>
85
+ <% if @processes.empty? %>
86
+ <p class="empty">No process has registered. Start one with <code>solid_objects start</code>.</p>
87
+ <% else %>
88
+ <table>
89
+ <thead>
90
+ <tr><th>Kind</th><th>Host</th><th>PID</th><th>State</th><th>Last heartbeat</th></tr>
91
+ </thead>
92
+ <tbody>
93
+ <% @processes.each do |process_record| %>
94
+ <tr>
95
+ <td><%= h(process_record.kind) %></td>
96
+ <td><%= h(process_record.hostname) %></td>
97
+ <td><%= h(process_record.pid) %></td>
98
+ <td><%= status_label(process_record.shutdown_state) %></td>
99
+ <td><%= relative_time(process_record.last_heartbeat_at) %></td>
100
+ </tr>
101
+ <% end %>
102
+ </tbody>
103
+ </table>
104
+ <% end %>
105
+ </section>
106
+
107
+ <section class="panel">
108
+ <h2>Recent dead letters</h2>
109
+ <% if @dead_letters.empty? %>
110
+ <p class="empty">No message has exhausted its attempts.</p>
111
+ <% else %>
112
+ <table>
113
+ <thead>
114
+ <tr><th>Actor</th><th>Operation</th><th>Exception</th><th>Last failed</th></tr>
115
+ </thead>
116
+ <tbody>
117
+ <% @dead_letters.each do |dead_letter| %>
118
+ <tr>
119
+ <td><a href="<%= h(path_to("/dead_letters/#{dead_letter.id}")) %>"><%= actor_label(dead_letter) %></a></td>
120
+ <td><%= h(dead_letter.operation) %></td>
121
+ <td><%= h(dead_letter.exception_class) %></td>
122
+ <td><%= relative_time(dead_letter.last_failed_at) %></td>
123
+ </tr>
124
+ <% end %>
125
+ </tbody>
126
+ </table>
127
+ <% end %>
128
+ </section>
@@ -0,0 +1,40 @@
1
+ <section class="panel">
2
+ <div class="panel-head">
3
+ <h2><%= h(@dead_letter.operation) %></h2>
4
+ <div class="actions">
5
+ <a href="<%= h(path_to("/instances/#{@dead_letter.instance_id}")) %>"><%= actor_label(@dead_letter) %></a>
6
+ <% if @dead_letter.retried_message_id %>
7
+ <a href="<%= h(path_to("/messages/#{@dead_letter.retried_message_id}")) %>">Retried</a>
8
+ <% else %>
9
+ <%= form_to("/dead_letters/#{@dead_letter.id}/retry") %>
10
+ <button type="submit">Retry</button>
11
+ </form>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+
16
+ <% if @error %>
17
+ <p class="note note-error">The retry was refused. <%= h(@error) %></p>
18
+ <% end %>
19
+
20
+ <p class="note">
21
+ A retry enqueues the original operation again under an idempotency key, so
22
+ pressing it twice produces one message rather than two.
23
+ </p>
24
+
25
+ <dl class="attributes">
26
+ <div><dt>Attempts</dt><dd><%= number(@dead_letter.attempts) %></dd></div>
27
+ <div><dt>Exception</dt><dd><%= h(@dead_letter.exception_class) %></dd></div>
28
+ <div><dt>First failed</dt><dd><%= relative_time(@dead_letter.first_failed_at) %></dd></div>
29
+ <div><dt>Last failed</dt><dd><%= relative_time(@dead_letter.last_failed_at) %></dd></div>
30
+ </dl>
31
+
32
+ <h3>Message</h3>
33
+ <pre class="payload"><%= h(truncate(@dead_letter.exception_message)) %></pre>
34
+
35
+ <h3>Arguments</h3>
36
+ <%= json_block(@dead_letter.arguments) %>
37
+
38
+ <h3>Backtrace</h3>
39
+ <pre class="payload"><%= h(truncate(Array(@dead_letter.backtrace).join("\n"))) %></pre>
40
+ </section>
@@ -0,0 +1,42 @@
1
+ <section class="panel">
2
+ <h2>Dead letters</h2>
3
+
4
+ <% if @paginator.records.empty? %>
5
+ <p class="empty">No message has exhausted its attempts.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr>
10
+ <th>Actor</th>
11
+ <th>Operation</th>
12
+ <th>Exception</th>
13
+ <th>Attempts</th>
14
+ <th>Last failed</th>
15
+ <th>Retry</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @paginator.records.each do |dead_letter| %>
20
+ <tr>
21
+ <td><a href="<%= h(path_to("/dead_letters/#{dead_letter.id}")) %>"><%= actor_label(dead_letter) %></a></td>
22
+ <td><%= h(dead_letter.operation) %></td>
23
+ <td><%= h(dead_letter.exception_class) %></td>
24
+ <td><%= number(dead_letter.attempts) %></td>
25
+ <td><%= relative_time(dead_letter.last_failed_at) %></td>
26
+ <td>
27
+ <% if dead_letter.retried_message_id %>
28
+ <a href="<%= h(path_to("/messages/#{dead_letter.retried_message_id}")) %>">retried</a>
29
+ <% else %>
30
+ <%= form_to("/dead_letters/#{dead_letter.id}/retry") %>
31
+ <button type="submit">Retry</button>
32
+ </form>
33
+ <% end %>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ <% end %>
40
+
41
+ <%= erb(:_paging) %>
42
+ </section>
@@ -0,0 +1,35 @@
1
+ <section class="panel">
2
+ <h2>Effects</h2>
3
+ <%= erb(:_status_filter, statuses: SolidObjects::Web::Statistics::EFFECT_STATUSES, selected: @status) %>
4
+
5
+ <% if @paginator.records.empty? %>
6
+ <p class="empty">No effect matches this filter.</p>
7
+ <% else %>
8
+ <table>
9
+ <thead>
10
+ <tr>
11
+ <th>Name</th>
12
+ <th>Actor</th>
13
+ <th>Status</th>
14
+ <th>Attempts</th>
15
+ <th>Available</th>
16
+ <th>Completed</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @paginator.records.each do |effect| %>
21
+ <tr>
22
+ <td><%= h(effect.name) %></td>
23
+ <td><a href="<%= h(path_to("/instances/#{effect.instance_id}")) %>">instance <%= number(effect.instance_id) %></a></td>
24
+ <td><%= status_label(effect.status) %></td>
25
+ <td><%= number(effect.attempt_count) %> / <%= number(effect.max_attempts) %></td>
26
+ <td><%= relative_time(effect.available_at) %></td>
27
+ <td><%= relative_time(effect.completed_at) %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ <% end %>
33
+
34
+ <%= erb(:_paging) %>
35
+ </section>
@@ -0,0 +1,139 @@
1
+ <section class="panel">
2
+ <div class="panel-head">
3
+ <h2><%= actor_label(@instance) %></h2>
4
+ <div class="actions">
5
+ <%= status_label(lease_state(@instance)) %>
6
+ <% if @instance.paused_at %>
7
+ <%= form_to("/instances/#{@instance.id}/resume") %>
8
+ <button type="submit">Resume</button>
9
+ </form>
10
+ <% else %>
11
+ <%= form_to("/instances/#{@instance.id}/pause") %>
12
+ <button type="submit" class="danger">Pause</button>
13
+ </form>
14
+ <% end %>
15
+ </div>
16
+ </div>
17
+
18
+ <p class="note">
19
+ A paused instance is not claimed again. A pass already in flight finishes its
20
+ turn, and a synchronous caller waiting on this instance times out rather than
21
+ receiving a result.
22
+ </p>
23
+
24
+ <dl class="attributes">
25
+ <div><dt>State version</dt><dd><%= number(@instance.state_version) %></dd></div>
26
+ <div><dt>Next sequence</dt><dd><%= number(@instance.next_message_sequence) %></dd></div>
27
+ <div><dt>Activation generation</dt><dd><%= number(@instance.activation_generation) %></dd></div>
28
+ <div><dt>Activation owner</dt><dd><%= h(@instance.activation_owner_id || "none") %></dd></div>
29
+ <div><dt>Lease expires</dt><dd><%= relative_time(@instance.activation_expires_at) %></dd></div>
30
+ <div><dt>Last used</dt><dd><%= relative_time(@instance.last_used_at) %></dd></div>
31
+ <div><dt>Last claimed</dt><dd><%= relative_time(@instance.last_claimed_at) %></dd></div>
32
+ <div><dt>Paused</dt><dd><%= relative_time(@instance.paused_at) %></dd></div>
33
+ </dl>
34
+
35
+ <h3>State</h3>
36
+ <%= json_block(@instance.state) %>
37
+ </section>
38
+
39
+ <section class="panel">
40
+ <h2>Mailbox</h2>
41
+ <%= erb(:_messages, title: "Ready", messages: @ready_messages, empty: "No message is waiting.") %>
42
+ <%= erb(:_messages, title: "Claimed", messages: @claimed_messages, empty: "No message is claimed.") %>
43
+ <%= erb(:_messages, title: "Recent", messages: @recent_messages, empty: "This instance has no message history.") %>
44
+ </section>
45
+
46
+ <section class="panel">
47
+ <h2>Reminders</h2>
48
+ <% if @reminders.empty? %>
49
+ <p class="empty">No reminder is scheduled.</p>
50
+ <% else %>
51
+ <table>
52
+ <thead>
53
+ <tr><th>Name</th><th>Operation</th><th>Status</th><th>Next run</th><th>Interval</th></tr>
54
+ </thead>
55
+ <tbody>
56
+ <% @reminders.each do |reminder| %>
57
+ <tr>
58
+ <td><%= h(reminder.name) %></td>
59
+ <td><%= h(reminder.operation) %></td>
60
+ <td><%= status_label(reminder.status) %></td>
61
+ <td><%= relative_time(reminder.next_run_at) %></td>
62
+ <td><%= duration(reminder.interval_seconds) %></td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ <% end %>
68
+ </section>
69
+
70
+ <section class="panel">
71
+ <h2>Effects</h2>
72
+ <% if @effects.empty? %>
73
+ <p class="empty">This instance has emitted no effect.</p>
74
+ <% else %>
75
+ <table>
76
+ <thead>
77
+ <tr><th>Name</th><th>Status</th><th>Attempts</th><th>Available</th><th>Completed</th></tr>
78
+ </thead>
79
+ <tbody>
80
+ <% @effects.each do |effect| %>
81
+ <tr>
82
+ <td><%= h(effect.name) %></td>
83
+ <td><%= status_label(effect.status) %></td>
84
+ <td><%= number(effect.attempt_count) %> / <%= number(effect.max_attempts) %></td>
85
+ <td><%= relative_time(effect.available_at) %></td>
86
+ <td><%= relative_time(effect.completed_at) %></td>
87
+ </tr>
88
+ <% end %>
89
+ </tbody>
90
+ </table>
91
+ <% end %>
92
+ </section>
93
+
94
+ <section class="panel">
95
+ <h2>Broadcasts</h2>
96
+ <% if @broadcasts.empty? %>
97
+ <p class="empty">This instance has broadcast nothing.</p>
98
+ <% else %>
99
+ <table>
100
+ <thead>
101
+ <tr><th>Observable</th><th>Status</th><th>State version</th><th>Attempts</th><th>Delivered</th></tr>
102
+ </thead>
103
+ <tbody>
104
+ <% @broadcasts.each do |broadcast| %>
105
+ <tr>
106
+ <td><%= h(broadcast.observable_name) %></td>
107
+ <td><%= status_label(broadcast.status) %></td>
108
+ <td><%= number(broadcast.state_version) %></td>
109
+ <td><%= number(broadcast.attempt_count) %></td>
110
+ <td><%= relative_time(broadcast.delivered_at) %></td>
111
+ </tr>
112
+ <% end %>
113
+ </tbody>
114
+ </table>
115
+ <% end %>
116
+ </section>
117
+
118
+ <section class="panel">
119
+ <h2>Dead letters</h2>
120
+ <% if @dead_letters.empty? %>
121
+ <p class="empty">No message has exhausted its attempts.</p>
122
+ <% else %>
123
+ <table>
124
+ <thead>
125
+ <tr><th>Operation</th><th>Exception</th><th>Attempts</th><th>Last failed</th></tr>
126
+ </thead>
127
+ <tbody>
128
+ <% @dead_letters.each do |dead_letter| %>
129
+ <tr>
130
+ <td><a href="<%= h(path_to("/dead_letters/#{dead_letter.id}")) %>"><%= h(dead_letter.operation) %></a></td>
131
+ <td><%= h(dead_letter.exception_class) %></td>
132
+ <td><%= number(dead_letter.attempts) %></td>
133
+ <td><%= relative_time(dead_letter.last_failed_at) %></td>
134
+ </tr>
135
+ <% end %>
136
+ </tbody>
137
+ </table>
138
+ <% end %>
139
+ </section>
@@ -0,0 +1,49 @@
1
+ <section class="panel">
2
+ <h2>Instances</h2>
3
+
4
+ <form class="filters-form" method="get" action="<%= h(path_to("/instances")) %>">
5
+ <label>
6
+ Actor type
7
+ <input type="search" name="actor_type" list="actor-types" value="<%= h(url_params("actor_type")) %>" />
8
+ </label>
9
+ <datalist id="actor-types">
10
+ <% @actor_types.each do |actor_type| %>
11
+ <option value="<%= h(actor_type) %>"></option>
12
+ <% end %>
13
+ </datalist>
14
+ <label>
15
+ Actor id contains
16
+ <input type="search" name="actor_id" value="<%= h(url_params("actor_id")) %>" />
17
+ </label>
18
+ <button type="submit">Filter</button>
19
+ </form>
20
+
21
+ <% if @paginator.records.empty? %>
22
+ <p class="empty">No instance matches this filter.</p>
23
+ <% else %>
24
+ <table>
25
+ <thead>
26
+ <tr>
27
+ <th>Actor</th>
28
+ <th>Lease</th>
29
+ <th>State version</th>
30
+ <th>Next sequence</th>
31
+ <th>Last used</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody>
35
+ <% @paginator.records.each do |instance| %>
36
+ <tr data-instance-row="<%= h(instance.id) %>">
37
+ <td><%= instance_link(instance) %></td>
38
+ <td><%= status_label(lease_state(instance)) %></td>
39
+ <td><%= number(instance.state_version) %></td>
40
+ <td><%= number(instance.next_message_sequence) %></td>
41
+ <td><%= relative_time(instance.last_used_at) %></td>
42
+ </tr>
43
+ <% end %>
44
+ </tbody>
45
+ </table>
46
+ <% end %>
47
+
48
+ <%= erb(:_paging) %>
49
+ </section>
@@ -0,0 +1,22 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="robots" content="noindex, nofollow" />
7
+ <title>Solid Objects</title>
8
+ <link rel="stylesheet" href="<%= h(path_to("/stylesheets/application.css")) %>" />
9
+ <script src="<%= h(path_to("/javascripts/application.js")) %>" nonce="<%= h(csp_nonce) %>" defer></script>
10
+ <% if SolidObjects::Web.charts? && current_tab?("/") %>
11
+ <script src="<%= h(chart_library_source) %>"<%= chart_library_integrity_attributes %> defer></script>
12
+ <script src="<%= h(path_to("/javascripts/charts.js")) %>" nonce="<%= h(csp_nonce) %>" defer></script>
13
+ <% end %>
14
+ </head>
15
+ <body data-stats-path="<%= h(path_to("/stats")) %>">
16
+ <%= erb(:_navigation) %>
17
+ <main class="page">
18
+ <%= erb(:_summary) %>
19
+ <%= locals.fetch(:content) %>
20
+ </main>
21
+ </body>
22
+ </html>
@@ -0,0 +1,35 @@
1
+ <section class="panel">
2
+ <h2>Mailbox</h2>
3
+ <%= erb(:_status_filter, statuses: SolidObjects::Web::Application::MAILBOX_MEMBERSHIPS, selected: @membership, all: false) %>
4
+
5
+ <% if @paginator.records.empty? %>
6
+ <p class="empty">No message is <%= h(@membership) %>.</p>
7
+ <% else %>
8
+ <table>
9
+ <thead>
10
+ <tr>
11
+ <th>Actor</th>
12
+ <th>Sequence</th>
13
+ <th>Operation</th>
14
+ <th>Delivery</th>
15
+ <th>Attempts</th>
16
+ <th>Available</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @paginator.records.each do |message| %>
21
+ <tr>
22
+ <td><a href="<%= h(path_to("/instances/#{message.instance_id}")) %>"><%= actor_label(message) %></a></td>
23
+ <td><a href="<%= h(path_to("/messages/#{message.id}")) %>"><%= number(message.sequence) %></a></td>
24
+ <td><%= h(message.operation) %></td>
25
+ <td><%= status_label(message.delivery_mode) %></td>
26
+ <td><%= number(message.attempt_count) %> / <%= number(message.max_attempts) %></td>
27
+ <td><%= relative_time(message.available_at) %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ <% end %>
33
+
34
+ <%= erb(:_paging) %>
35
+ </section>