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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +72 -6
- data/app/models/solid_objects/broadcast.rb +8 -0
- data/docs/adr/0009-realtime-updates.md +5 -1
- data/docs/architecture.md +4 -2
- data/docs/authorization.md +11 -4
- data/docs/correctness.md +3 -3
- data/docs/dashboard.md +200 -0
- data/docs/database-schema.md +5 -4
- data/docs/development.md +12 -0
- data/docs/realtime.md +37 -7
- data/docs/roadmap.md +28 -6
- data/docs/security.md +8 -0
- data/examples/application/README.md +5 -5
- data/examples/application/app/actors/chat_room_actor.rb +1 -1
- data/examples/application/app/actors/shopping_cart_actor.rb +5 -5
- data/lib/solid_objects/actor.rb +6 -5
- data/lib/solid_objects/actor_channel.rb +7 -4
- data/lib/solid_objects/actor_definition.rb +15 -3
- data/lib/solid_objects/actor_view.rb +6 -1
- data/lib/solid_objects/effect_executor.rb +10 -2
- data/lib/solid_objects/errors.rb +3 -0
- data/lib/solid_objects/executor.rb +7 -1
- data/lib/solid_objects/reminder_scheduler.rb +28 -14
- data/lib/solid_objects/test_helper.rb +16 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +3 -3
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/web/action.rb +109 -0
- data/lib/solid_objects/web/application.rb +239 -0
- data/lib/solid_objects/web/csrf_protection.rb +130 -0
- data/lib/solid_objects/web/helpers.rb +227 -0
- data/lib/solid_objects/web/paginator.rb +64 -0
- data/lib/solid_objects/web/route.rb +55 -0
- data/lib/solid_objects/web/router.rb +46 -0
- data/lib/solid_objects/web/statistics.rb +78 -0
- data/lib/solid_objects/web.rb +222 -0
- data/sig/generated/lib/solid_objects/actor.rbs +2 -2
- data/sig/generated/lib/solid_objects/actor_definition.rbs +9 -2
- data/sig/generated/lib/solid_objects/errors.rbs +3 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +9 -6
- data/sig/generated/lib/solid_objects/test_helper.rbs +3 -0
- data/sig/generated/lib/solid_objects/web/action.rbs +78 -0
- data/sig/generated/lib/solid_objects/web/application.rbs +50 -0
- data/sig/generated/lib/solid_objects/web/csrf_protection.rbs +55 -0
- data/sig/generated/lib/solid_objects/web/helpers.rbs +118 -0
- data/sig/generated/lib/solid_objects/web/paginator.rbs +54 -0
- data/sig/generated/lib/solid_objects/web/route.rbs +45 -0
- data/sig/generated/lib/solid_objects/web/router.rbs +29 -0
- data/sig/generated/lib/solid_objects/web/statistics.rbs +46 -0
- data/sig/generated/lib/solid_objects/web.rbs +129 -0
- data/sig/generated/models/solid_objects/broadcast.rbs +2 -0
- data/web/assets/javascripts/application.js +118 -0
- data/web/assets/javascripts/charts.js +190 -0
- data/web/assets/stylesheets/application.css +527 -0
- data/web/views/_messages.erb +29 -0
- data/web/views/_navigation.erb +15 -0
- data/web/views/_paging.erb +17 -0
- data/web/views/_status_filter.erb +8 -0
- data/web/views/_summary.erb +39 -0
- data/web/views/broadcasts.erb +35 -0
- data/web/views/dashboard.erb +128 -0
- data/web/views/dead_letter.erb +40 -0
- data/web/views/dead_letters.erb +42 -0
- data/web/views/effects.erb +35 -0
- data/web/views/instance.erb +139 -0
- data/web/views/instances.erb +49 -0
- data/web/views/layout.erb +22 -0
- data/web/views/mailbox.erb +35 -0
- data/web/views/message.erb +34 -0
- data/web/views/processes.erb +37 -0
- data/web/views/reminders.erb +37 -0
- 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) %>–<%= 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>
|