acta 0.2.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 +7 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +210 -0
- data/LICENSE +21 -0
- data/PLAN.md +158 -0
- data/README.md +559 -0
- data/Rakefile +12 -0
- data/app/controllers/acta/web/application_controller.rb +10 -0
- data/app/controllers/acta/web/events_controller.rb +37 -0
- data/app/helpers/acta/web/application_helper.rb +106 -0
- data/app/views/acta/web/events/index.html.erb +312 -0
- data/app/views/acta/web/events/show.html.erb +72 -0
- data/app/views/layouts/acta/web/application.html.erb +594 -0
- data/config/routes.rb +4 -0
- data/lib/acta/actor.rb +34 -0
- data/lib/acta/adapters/base.rb +59 -0
- data/lib/acta/adapters/postgres.rb +73 -0
- data/lib/acta/adapters/sqlite.rb +58 -0
- data/lib/acta/adapters.rb +19 -0
- data/lib/acta/array_type.rb +30 -0
- data/lib/acta/command.rb +48 -0
- data/lib/acta/current.rb +10 -0
- data/lib/acta/errors.rb +102 -0
- data/lib/acta/event.rb +80 -0
- data/lib/acta/events_query.rb +73 -0
- data/lib/acta/handler.rb +9 -0
- data/lib/acta/model.rb +58 -0
- data/lib/acta/model_type.rb +32 -0
- data/lib/acta/projection.rb +64 -0
- data/lib/acta/projection_managed.rb +108 -0
- data/lib/acta/railtie.rb +65 -0
- data/lib/acta/reactor.rb +15 -0
- data/lib/acta/reactor_job.rb +19 -0
- data/lib/acta/record.rb +10 -0
- data/lib/acta/schema.rb +12 -0
- data/lib/acta/serializable.rb +48 -0
- data/lib/acta/testing/dsl.rb +90 -0
- data/lib/acta/testing/matchers.rb +77 -0
- data/lib/acta/testing.rb +50 -0
- data/lib/acta/types/encrypted_string.rb +63 -0
- data/lib/acta/version.rb +5 -0
- data/lib/acta/web/engine.rb +13 -0
- data/lib/acta/web/events_query.rb +81 -0
- data/lib/acta/web.rb +45 -0
- data/lib/acta.rb +296 -0
- data/lib/generators/acta/install/install_generator.rb +23 -0
- data/lib/generators/acta/install/templates/create_acta_events.rb.tt +9 -0
- data/sig/acta.rbs +4 -0
- metadata +152 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Acta
|
|
7
|
+
module Web
|
|
8
|
+
module ApplicationHelper
|
|
9
|
+
def acta_chip_hue(event_type)
|
|
10
|
+
h = event_type.to_s.chars.reduce(0) { |acc, c| ((acc * 31 + c.ord) & 0xFFFFFFFF) }
|
|
11
|
+
h.abs % 360
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def acta_dot_color(event_type)
|
|
15
|
+
"oklch(0.70 0.14 #{acta_chip_hue(event_type)})"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def acta_fmt_time(time)
|
|
19
|
+
return "-" unless time
|
|
20
|
+
t = time.respond_to?(:utc) ? time.utc : Time.parse(time.to_s).utc
|
|
21
|
+
ms = t.strftime("%3N")
|
|
22
|
+
t.strftime("%H:%M:%S") + ".#{ms}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def acta_fmt_abs(time)
|
|
26
|
+
return "-" unless time
|
|
27
|
+
t = time.respond_to?(:utc) ? time.utc : Time.parse(time.to_s).utc
|
|
28
|
+
ms = t.strftime("%3N")
|
|
29
|
+
t.strftime("%Y-%m-%d %H:%M:%S") + ".#{ms}Z"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def acta_preview_payload(payload)
|
|
33
|
+
return "{}" unless payload.is_a?(Hash) && payload.any?
|
|
34
|
+
masked = acta_mask_encrypted(payload)
|
|
35
|
+
masked.keys.first(3).map { |k| "#{k}=#{masked[k].inspect}" }.join(" ")
|
|
36
|
+
rescue StandardError
|
|
37
|
+
"{}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def acta_pretty_json(obj)
|
|
41
|
+
JSON.pretty_generate(acta_mask_encrypted(obj))
|
|
42
|
+
rescue StandardError
|
|
43
|
+
obj.to_s
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Replace any AR::Encryption-recognized ciphertext leaf in `obj`
|
|
47
|
+
# with "********". Walks hashes and arrays; non-encrypted scalars
|
|
48
|
+
# pass through unchanged. Used by the admin UI so encrypted payload
|
|
49
|
+
# values aren't displayed as raw ciphertext envelopes.
|
|
50
|
+
ACTA_MASK = "********"
|
|
51
|
+
|
|
52
|
+
def acta_mask_encrypted(obj)
|
|
53
|
+
case obj
|
|
54
|
+
when Hash then obj.each_with_object({}) { |(k, v), acc| acc[k] = acta_mask_encrypted(v) }
|
|
55
|
+
when Array then obj.map { |v| acta_mask_encrypted(v) }
|
|
56
|
+
when String then acta_encrypted_value?(obj) ? ACTA_MASK : obj
|
|
57
|
+
else obj
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def acta_encrypted_value?(value)
|
|
62
|
+
return false unless defined?(ActiveRecord::Encryption)
|
|
63
|
+
|
|
64
|
+
ActiveRecord::Encryption.encryptor.encrypted?(value)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Build a URL for the events index, merging +overrides+ into current params.
|
|
70
|
+
# Pass nil for a key to remove it. Resets page when filters change.
|
|
71
|
+
def acta_filter_url(overrides = {})
|
|
72
|
+
current = {
|
|
73
|
+
event_type: params[:event_type],
|
|
74
|
+
stream_type: params[:stream_type],
|
|
75
|
+
actor_id: params[:actor_id],
|
|
76
|
+
stream_key: params[:stream_key],
|
|
77
|
+
q: params[:q],
|
|
78
|
+
selected: params[:selected],
|
|
79
|
+
page: params[:page]
|
|
80
|
+
}.compact_blank
|
|
81
|
+
|
|
82
|
+
overrides = overrides.transform_keys(&:to_sym)
|
|
83
|
+
filter_keys = %i[event_type stream_type actor_id stream_key q]
|
|
84
|
+
current.delete(:page) if (overrides.keys & filter_keys).any?
|
|
85
|
+
current.delete(:selected) if (overrides.keys & filter_keys).any?
|
|
86
|
+
|
|
87
|
+
merged = current.merge(overrides)
|
|
88
|
+
merged.compact_blank!
|
|
89
|
+
merged.delete(:page) if merged[:page].to_s == "0"
|
|
90
|
+
|
|
91
|
+
encode_params(merged)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def encode_params(hash)
|
|
97
|
+
query = hash.map { |k, v| "#{enc(k)}=#{enc(v)}" }.join("&")
|
|
98
|
+
query.empty? ? request.path : "#{request.path}?#{query}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def enc(val)
|
|
102
|
+
URI.encode_www_form_component(val.to_s)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<%# ── Header ─────────────────────────────────────────────────────────── %>
|
|
2
|
+
<header class="acta-header">
|
|
3
|
+
<a href="<%= acta_filter_url(event_type: nil, stream_type: nil, actor_id: nil, stream_key: nil, q: nil, selected: nil, page: nil) %>" class="acta-logo">
|
|
4
|
+
<div class="acta-logo-dot"></div>
|
|
5
|
+
acta<span class="acta-dim acta-sans" style="font-weight:400">/log</span>
|
|
6
|
+
</a>
|
|
7
|
+
|
|
8
|
+
<form method="get" action="<%= request.path %>" class="acta-filter-bar acta-filter-form">
|
|
9
|
+
<%# Preserve facet filters across text-search submissions %>
|
|
10
|
+
<% %i[event_type stream_type actor_id].each do |f| %>
|
|
11
|
+
<% if params[f].present? %>
|
|
12
|
+
<input type="hidden" name="<%= f %>" value="<%= h params[f] %>">
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<div class="acta-filter-group" style="flex:1 1 320px;max-width:480px;">
|
|
17
|
+
<span class="acta-dim" style="font-size:13px">⌕</span>
|
|
18
|
+
<input
|
|
19
|
+
type="search"
|
|
20
|
+
name="q"
|
|
21
|
+
value="<%= h params[:q] %>"
|
|
22
|
+
placeholder="filter… event type, actor, stream"
|
|
23
|
+
class="acta-filter-input acta-filter-input-q"
|
|
24
|
+
autocomplete="off"
|
|
25
|
+
spellcheck="false"
|
|
26
|
+
>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="acta-filter-group" style="flex:0 1 240px;min-width:160px;">
|
|
30
|
+
<span class="acta-label acta-dim" style="font-size:10px;font-family:Inter,sans-serif;text-transform:uppercase;letter-spacing:0.6px;white-space:nowrap">stream_key</span>
|
|
31
|
+
<input
|
|
32
|
+
type="search"
|
|
33
|
+
name="stream_key"
|
|
34
|
+
value="<%= h params[:stream_key] %>"
|
|
35
|
+
placeholder="grep stream id…"
|
|
36
|
+
class="acta-filter-input acta-filter-input-sk<%= ' has-value' if params[:stream_key].present? %>"
|
|
37
|
+
autocomplete="off"
|
|
38
|
+
spellcheck="false"
|
|
39
|
+
>
|
|
40
|
+
<% if params[:stream_key].present? %>
|
|
41
|
+
<a href="<%= acta_filter_url(stream_key: nil) %>" class="acta-clear-btn">✕</a>
|
|
42
|
+
<% end %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<button type="submit" class="acta-filter-submit acta-sans">Filter</button>
|
|
46
|
+
</form>
|
|
47
|
+
|
|
48
|
+
<div class="acta-status">
|
|
49
|
+
<span><span class="acta-status-dot">●</span> live</span>
|
|
50
|
+
<span><%= @base_count.to_s(:delimited) rescue @base_count %> events</span>
|
|
51
|
+
</div>
|
|
52
|
+
</header>
|
|
53
|
+
|
|
54
|
+
<%# ── Body ───────────────────────────────────────────────────────────── %>
|
|
55
|
+
<div class="acta-body">
|
|
56
|
+
|
|
57
|
+
<%# ── Left rail: facets ──────────────────────────────────────────── %>
|
|
58
|
+
<aside class="acta-rail">
|
|
59
|
+
<details class="acta-facet" open>
|
|
60
|
+
<summary>event_type</summary>
|
|
61
|
+
<% @facet_event_type.each do |event_type, count| %>
|
|
62
|
+
<a
|
|
63
|
+
href="<%= acta_filter_url(event_type: params[:event_type] == event_type ? nil : event_type) %>"
|
|
64
|
+
class="acta-facet-item<%= ' active' if params[:event_type] == event_type %>"
|
|
65
|
+
>
|
|
66
|
+
<span class="acta-facet-dot acta-dot" style="background:<%= acta_dot_color(event_type) %>"></span>
|
|
67
|
+
<span class="acta-facet-name"><%= event_type %></span>
|
|
68
|
+
<span class="acta-facet-count"><%= count %></span>
|
|
69
|
+
</a>
|
|
70
|
+
<% end %>
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<details class="acta-facet" open>
|
|
74
|
+
<summary>stream_type</summary>
|
|
75
|
+
<% @facet_stream_type.each do |stream_type, count| %>
|
|
76
|
+
<a
|
|
77
|
+
href="<%= acta_filter_url(stream_type: params[:stream_type] == stream_type ? nil : stream_type) %>"
|
|
78
|
+
class="acta-facet-item<%= ' active' if params[:stream_type] == stream_type %>"
|
|
79
|
+
>
|
|
80
|
+
<span class="acta-facet-name"><%= stream_type %></span>
|
|
81
|
+
<span class="acta-facet-count"><%= count %></span>
|
|
82
|
+
</a>
|
|
83
|
+
<% end %>
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
<details class="acta-facet" open>
|
|
87
|
+
<summary>actor_id</summary>
|
|
88
|
+
<% @facet_actor_id.each do |actor_id, count| %>
|
|
89
|
+
<a
|
|
90
|
+
href="<%= acta_filter_url(actor_id: params[:actor_id] == actor_id ? nil : actor_id) %>"
|
|
91
|
+
class="acta-facet-item<%= ' active' if params[:actor_id] == actor_id %>"
|
|
92
|
+
>
|
|
93
|
+
<span class="acta-facet-name"><%= actor_id %></span>
|
|
94
|
+
<span class="acta-facet-count"><%= count %></span>
|
|
95
|
+
</a>
|
|
96
|
+
<% end %>
|
|
97
|
+
</details>
|
|
98
|
+
</aside>
|
|
99
|
+
|
|
100
|
+
<%# ── Main pane ──────────────────────────────────────────────────── %>
|
|
101
|
+
<main class="acta-main">
|
|
102
|
+
|
|
103
|
+
<%# Active filter chips -%>
|
|
104
|
+
<div class="acta-chips">
|
|
105
|
+
<% if @active_filters.empty? %>
|
|
106
|
+
<span class="acta-dim acta-sans">no filters · <%= @base_count %> events</span>
|
|
107
|
+
<% else %>
|
|
108
|
+
<% if params[:q].present? %>
|
|
109
|
+
<span class="acta-chip acta-sans">
|
|
110
|
+
q: <span class="acta-chip-val"><%= h params[:q] %></span>
|
|
111
|
+
<a href="<%= acta_filter_url(q: nil) %>" class="acta-chip-x">✕</a>
|
|
112
|
+
</span>
|
|
113
|
+
<% end %>
|
|
114
|
+
<% { event_type: params[:event_type], stream_type: params[:stream_type], actor_id: params[:actor_id], stream_key: params[:stream_key] }.compact_blank.each do |key, value| %>
|
|
115
|
+
<span class="acta-chip acta-sans">
|
|
116
|
+
<%= key %>: <span class="acta-chip-val"><%= h value %></span>
|
|
117
|
+
<a href="<%= acta_filter_url(key => nil) %>" class="acta-chip-x">✕</a>
|
|
118
|
+
</span>
|
|
119
|
+
<% end %>
|
|
120
|
+
<span class="acta-chips-count acta-sans"><%= @filtered_count %> of <%= @base_count %></span>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<%# Log rows -%>
|
|
125
|
+
<div class="acta-log">
|
|
126
|
+
<% if @base_count == 0 %>
|
|
127
|
+
<%# ── Empty install state ────────────────────────────────────── %>
|
|
128
|
+
<div class="acta-empty-install">
|
|
129
|
+
<div class="acta-empty-badge">
|
|
130
|
+
<span class="acta-dot" style="background:var(--dim)"></span>
|
|
131
|
+
empty log
|
|
132
|
+
</div>
|
|
133
|
+
<div class="acta-empty-title">No events have been recorded yet.</div>
|
|
134
|
+
<p class="acta-empty-desc">
|
|
135
|
+
Acta is wired up — the <code>events</code> table is empty.
|
|
136
|
+
Set an actor at your request boundary and emit your first event,
|
|
137
|
+
then reload this page.
|
|
138
|
+
</p>
|
|
139
|
+
|
|
140
|
+
<div class="acta-empty-steps">
|
|
141
|
+
<div>
|
|
142
|
+
<div class="acta-step-label">
|
|
143
|
+
<span class="acta-step-num">1</span>
|
|
144
|
+
Set the actor at your request boundary
|
|
145
|
+
</div>
|
|
146
|
+
<pre class="acta-code-block"># app/controllers/application_controller.rb
|
|
147
|
+
before_action do
|
|
148
|
+
Acta::Current.actor = Acta::Actor.new(
|
|
149
|
+
type: "user",
|
|
150
|
+
id: current_user.id,
|
|
151
|
+
source: "web"
|
|
152
|
+
)
|
|
153
|
+
end</pre>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div>
|
|
157
|
+
<div class="acta-step-label">
|
|
158
|
+
<span class="acta-step-num">2</span>
|
|
159
|
+
Emit an event
|
|
160
|
+
</div>
|
|
161
|
+
<pre class="acta-code-block">Acta.emit(OrderPlaced.new(
|
|
162
|
+
order_id: "o_1",
|
|
163
|
+
customer_id: "c_1",
|
|
164
|
+
total_cents: 4200
|
|
165
|
+
))</pre>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div>
|
|
169
|
+
<div class="acta-step-label">
|
|
170
|
+
<span class="acta-step-num">3</span>
|
|
171
|
+
Reload — the row appears here.
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="acta-empty-footer">
|
|
177
|
+
<a href="https://github.com/whoojemaflip/acta#readme" target="_blank" rel="noopener" class="acta-empty-link">↗ README</a>
|
|
178
|
+
<a href="https://github.com/whoojemaflip/acta#defining-events" target="_blank" rel="noopener" class="acta-empty-link">↗ Defining events</a>
|
|
179
|
+
<a href="https://github.com/whoojemaflip/acta#commands" target="_blank" rel="noopener" class="acta-empty-link">↗ Commands</a>
|
|
180
|
+
<span class="acta-empty-footer-status">
|
|
181
|
+
<span class="acta-status-dot">●</span> tailing · 0 events
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<% elsif @events.empty? %>
|
|
187
|
+
<%# ── Zero matches ────────────────────────────────────────────── %>
|
|
188
|
+
<div class="acta-empty-filters">
|
|
189
|
+
<strong>0 matches</strong>
|
|
190
|
+
Loosen filters or <a href="<%= acta_filter_url(event_type: nil, stream_type: nil, actor_id: nil, stream_key: nil, q: nil) %>" style="color:var(--accent);text-decoration:none">clear them</a>.
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<% else %>
|
|
194
|
+
<%# ── Event rows ──────────────────────────────────────────────── %>
|
|
195
|
+
<% @events.each do |event| %>
|
|
196
|
+
<div
|
|
197
|
+
class="acta-row<%= ' selected' if params[:selected] == event.uuid %>"
|
|
198
|
+
data-href="<%= acta_filter_url(selected: event.uuid) %>"
|
|
199
|
+
>
|
|
200
|
+
<span class="acta-row-time" title="<%= acta_fmt_abs(event.recorded_at) %>">
|
|
201
|
+
<%= acta_fmt_time(event.recorded_at) %>
|
|
202
|
+
</span>
|
|
203
|
+
|
|
204
|
+
<span class="acta-row-dot">
|
|
205
|
+
<span class="acta-dot" style="background:<%= acta_dot_color(event.event_type) %>"></span>
|
|
206
|
+
</span>
|
|
207
|
+
|
|
208
|
+
<span class="acta-row-type"><%= event.event_type %></span>
|
|
209
|
+
|
|
210
|
+
<span class="acta-row-stream">
|
|
211
|
+
<span class="acta-dim"><%= event.stream_type %></span>
|
|
212
|
+
<span class="acta-dim" style="opacity:0.5">/</span><%
|
|
213
|
+
%><a
|
|
214
|
+
href="<%= acta_filter_url(stream_key: event.stream_key) %>"
|
|
215
|
+
class="acta-stream-key"
|
|
216
|
+
title="grep stream_key = <%= h event.stream_key %>"
|
|
217
|
+
onClick="event.stopPropagation()"
|
|
218
|
+
><%= event.stream_key %></a><%
|
|
219
|
+
if event.payload.present? %>
|
|
220
|
+
<span class="acta-payload-preview acta-dim"><%= acta_preview_payload(event.payload) %></span><%
|
|
221
|
+
end %>
|
|
222
|
+
</span>
|
|
223
|
+
|
|
224
|
+
<span class="acta-row-actor">
|
|
225
|
+
<%= event.actor_id %><% if event.source.present? %> <span style="opacity:0.5">·</span> <%= event.source %><% end %>
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<% end %>
|
|
229
|
+
<% end %>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<%# Pagination strip -%>
|
|
233
|
+
<div class="acta-pagination">
|
|
234
|
+
<span class="acta-sans"><%= @filtered_count %> matches</span>
|
|
235
|
+
<span class="acta-dim">·</span>
|
|
236
|
+
<span class="acta-sans">page <%= @page + 1 %>/<%= @total_pages %></span>
|
|
237
|
+
<div class="acta-pagination-btns">
|
|
238
|
+
<% if @page > 0 %>
|
|
239
|
+
<a href="<%= acta_filter_url(page: @page - 1) %>" class="acta-page-btn acta-sans">↑ newer</a>
|
|
240
|
+
<% else %>
|
|
241
|
+
<span class="acta-page-btn acta-sans disabled">↑ newer</span>
|
|
242
|
+
<% end %>
|
|
243
|
+
<% if @page < @total_pages - 1 %>
|
|
244
|
+
<a href="<%= acta_filter_url(page: @page + 1) %>" class="acta-page-btn acta-sans">older ↓</a>
|
|
245
|
+
<% else %>
|
|
246
|
+
<span class="acta-page-btn acta-sans disabled">older ↓</span>
|
|
247
|
+
<% end %>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</main>
|
|
251
|
+
|
|
252
|
+
<%# ── Right detail panel (when a row is selected) ─────────────────── %>
|
|
253
|
+
<% if @selected_event %>
|
|
254
|
+
<% ev = @selected_event %>
|
|
255
|
+
<aside class="acta-detail">
|
|
256
|
+
<div class="acta-detail-header">
|
|
257
|
+
<span class="acta-dot" style="background:<%= acta_dot_color(ev.event_type) %>"></span>
|
|
258
|
+
<span class="acta-detail-type"><%= ev.event_type %></span>
|
|
259
|
+
<span class="acta-detail-version">v<%= ev.event_version %></span>
|
|
260
|
+
<a href="<%= acta_filter_url(selected: nil) %>" class="acta-detail-close" title="Close">✕</a>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div class="acta-detail-body">
|
|
264
|
+
<div class="acta-kv">
|
|
265
|
+
<span class="acta-kv-key">uuid</span>
|
|
266
|
+
<span class="acta-kv-val"><%= ev.uuid %></span>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="acta-kv">
|
|
269
|
+
<span class="acta-kv-key">recorded_at</span>
|
|
270
|
+
<span class="acta-kv-val"><%= acta_fmt_abs(ev.recorded_at) %></span>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="acta-kv">
|
|
273
|
+
<span class="acta-kv-key">occurred_at</span>
|
|
274
|
+
<span class="acta-kv-val"><%= acta_fmt_abs(ev.occurred_at) %></span>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="acta-kv">
|
|
277
|
+
<span class="acta-kv-key">stream</span>
|
|
278
|
+
<span class="acta-kv-val">
|
|
279
|
+
<a href="<%= acta_filter_url(stream_type: ev.stream_type, stream_key: ev.stream_key, selected: nil) %>">
|
|
280
|
+
<%= ev.stream_type %>/<%= ev.stream_key %><% if ev.respond_to?(:stream_sequence) && ev.stream_sequence.present? %> #<%= ev.stream_sequence %><% end %>
|
|
281
|
+
</a>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
<div class="acta-kv">
|
|
285
|
+
<span class="acta-kv-key">actor</span>
|
|
286
|
+
<span class="acta-kv-val">
|
|
287
|
+
<a href="<%= acta_filter_url(actor_id: ev.actor_id, selected: nil) %>"><%= ev.actor_type %>/<%= ev.actor_id %></a>
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
<div class="acta-kv">
|
|
291
|
+
<span class="acta-kv-key">source</span>
|
|
292
|
+
<span class="acta-kv-val"><%= ev.source %></span>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="acta-section-label">payload</div>
|
|
296
|
+
<pre class="acta-json-block"><%= acta_pretty_json(ev.payload) %></pre>
|
|
297
|
+
|
|
298
|
+
<% if ev.respond_to?(:metadata) && ev.metadata.present? && ev.metadata != {} %>
|
|
299
|
+
<div class="acta-section-label">metadata</div>
|
|
300
|
+
<pre class="acta-json-block"><%= acta_pretty_json(ev.metadata) %></pre>
|
|
301
|
+
<% end %>
|
|
302
|
+
|
|
303
|
+
<div style="margin-top:18px">
|
|
304
|
+
<a href="<%= url_for(controller: "acta/web/events", action: "show", id: ev.uuid) %>" class="acta-sans" style="color:var(--accent);text-decoration:none;font-size:10px">
|
|
305
|
+
↗ permalink
|
|
306
|
+
</a>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</aside>
|
|
310
|
+
<% end %>
|
|
311
|
+
|
|
312
|
+
</div>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<%# Standalone event detail view — deep-linkable permalink for a single event %>
|
|
2
|
+
|
|
3
|
+
<header class="acta-header">
|
|
4
|
+
<a href="<%= url_for(controller: "acta/web/events", action: "index") %>" class="acta-logo">
|
|
5
|
+
<div class="acta-logo-dot"></div>
|
|
6
|
+
acta<span class="acta-dim acta-sans" style="font-weight:400">/log</span>
|
|
7
|
+
</a>
|
|
8
|
+
<span class="acta-dim" style="margin-left:8px;font-size:11px">·</span>
|
|
9
|
+
<span class="acta-dim" style="font-size:11px;margin-left:8px"><%= @event.event_type %></span>
|
|
10
|
+
<span class="acta-dim" style="margin-left:auto;font-size:10px;font-family:Inter,sans-serif">
|
|
11
|
+
<a href="<%= url_for(controller: "acta/web/events", action: "index", selected: @event.uuid) %>" style="color:var(--accent);text-decoration:none">
|
|
12
|
+
← back to log
|
|
13
|
+
</a>
|
|
14
|
+
</span>
|
|
15
|
+
</header>
|
|
16
|
+
|
|
17
|
+
<div class="acta-body">
|
|
18
|
+
<main class="acta-main" style="max-width:700px;padding:24px 32px;overflow-y:auto;">
|
|
19
|
+
|
|
20
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px;">
|
|
21
|
+
<span class="acta-dot" style="background:<%= acta_dot_color(@event.event_type) %>;width:10px;height:10px;"></span>
|
|
22
|
+
<span style="font-size:16px;color:var(--fg);"><%= @event.event_type %></span>
|
|
23
|
+
<span class="acta-sans acta-dim" style="font-size:11px;">v<%= @event.event_version %></span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="acta-kv">
|
|
27
|
+
<span class="acta-kv-key">uuid</span>
|
|
28
|
+
<span class="acta-kv-val"><%= @event.uuid %></span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="acta-kv">
|
|
31
|
+
<span class="acta-kv-key">recorded_at</span>
|
|
32
|
+
<span class="acta-kv-val"><%= acta_fmt_abs(@event.recorded_at) %></span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="acta-kv">
|
|
35
|
+
<span class="acta-kv-key">occurred_at</span>
|
|
36
|
+
<span class="acta-kv-val"><%= acta_fmt_abs(@event.occurred_at) %></span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="acta-kv">
|
|
39
|
+
<span class="acta-kv-key">stream</span>
|
|
40
|
+
<span class="acta-kv-val">
|
|
41
|
+
<a href="<%= url_for(controller: "acta/web/events", action: "index", stream_type: @event.stream_type, stream_key: @event.stream_key) %>">
|
|
42
|
+
<%= @event.stream_type %>/<%= @event.stream_key %><% if @event.respond_to?(:stream_sequence) && @event.stream_sequence.present? %> #<%= @event.stream_sequence %><% end %>
|
|
43
|
+
</a>
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="acta-kv">
|
|
47
|
+
<span class="acta-kv-key">actor</span>
|
|
48
|
+
<span class="acta-kv-val">
|
|
49
|
+
<a href="<%= url_for(controller: "acta/web/events", action: "index", actor_id: @event.actor_id) %>">
|
|
50
|
+
<%= @event.actor_type %>/<%= @event.actor_id %>
|
|
51
|
+
</a>
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="acta-kv">
|
|
55
|
+
<span class="acta-kv-key">source</span>
|
|
56
|
+
<span class="acta-kv-val"><%= @event.source %></span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="acta-kv">
|
|
59
|
+
<span class="acta-kv-key">event_version</span>
|
|
60
|
+
<span class="acta-kv-val"><%= @event.event_version %></span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="acta-section-label">payload</div>
|
|
64
|
+
<pre class="acta-json-block"><%= acta_pretty_json(@event.payload) %></pre>
|
|
65
|
+
|
|
66
|
+
<% if @event.respond_to?(:metadata) && @event.metadata.present? && @event.metadata != {} %>
|
|
67
|
+
<div class="acta-section-label">metadata</div>
|
|
68
|
+
<pre class="acta-json-block"><%= acta_pretty_json(@event.metadata) %></pre>
|
|
69
|
+
<% end %>
|
|
70
|
+
|
|
71
|
+
</main>
|
|
72
|
+
</div>
|