We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Related
Jump to file
- ∟ README.md
- ∟ assets/js/admin.js
- ∟ assets/js/hooks/bar_hook.js
- ∟ assets/js/hooks/funnel_hook.js
- ∟ assets/js/hooks/pie_hook.js
- ∟ lib/my_app/accounts.ex
- ∟ lib/my_app/events.ex
- ∟ lib/my_app/events/catalog.ex
- ∟ lib/my_app/events/event.ex
- ∟ lib/my_app/events/handler.ex
- ∟ lib/my_app_admin/query/events.ex
- ∟ lib/my_app_admin_web.ex
- ∟ lib/my_app_admin_web/components/chart_components.ex
- ∟ lib/my_app_admin_web/components/core_components.ex
- ∟ lib/my_app_admin_web/components/layouts.ex
- ∟ lib/my_app_admin_web/live/admin_user_live/index.ex
- ∟ lib/my_app_admin_web/live/dashboard_live.ex
- ∟ lib/my_app_admin_web/live/event_live/index.ex
- ∟ lib/my_app_admin_web/live/event_live/show.ex
- ∟ lib/my_app_admin_web/live/user_live/index.ex
- ∟ lib/my_app_admin_web/live/user_live/show.ex
- ∟ lib/my_app_web/router.ex
- ∟ priv/repo/migrations/0002_create_events.exs
- ∟ test/my_app/accounts_test.exs
- ∟ test/my_app/events_test.exs
- ∟ test/my_app_admin/query/events_test.exs
- ∟ test/support/fixtures/events_fixtures.ex
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Admin JS
The
Bar
,
Funnel
, and
Pie
hooks are registered alongside the existing
CopyToClipboard
hook in the admin LiveSocket.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
| - |
|
| + |
|
|
|
|
|
|
Bar chart hook
Handles tooltip show/hide/position for bar and stacked bar charts. Formats date labels into human-readable strings (e.g. "Mon, 3 Mar 2026" for days, "March 2026" for months). Reads
data-bar-label
,
data-bar-count
, and
data-bar-dataset
attributes from each bar section. The activity heatmap reuses this hook unchanged — its day cells carry the same
data-bar-*
attributes.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Funnel chart hook
Similar tooltip handling for funnel steps. Reads
data-funnel-label
,
data-funnel-count
, and
data-funnel-rate
attributes.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Pie chart hook
Tooltip handling for pie and donut slices. Reads
data-pie-label
,
data-pie-count
, and
data-pie-pct
attributes so the tooltip can show both the absolute count and the slice's share.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Tracking events in the app
Two events are wired into the registration flow:
register_user/1
tracks
"user.signed_up"
after a successful insert, using
with
to preserve the original
{:ok, user}
return value.
login_user_by_magic_link/1
tracks
"user.confirmed"
in the branch where a previously unconfirmed user clicks their magic link. Already-confirmed users skip the tracking.
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| - |
|
| - |
|
| - |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| - |
|
| - |
|
| - |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Events context
The public API is intentionally small:
track/2
,
subscribe/0
, and
broadcast/1
. All query and analytics functions live in the admin layer.
track/2
inserts the event, broadcasts it over PubSub for the real-time feed, and notifies any registered handlers. Failures are logged rather than raised, so tracking never breaks the calling code path.
Handlers are notified synchronously after each event insert. Register them in config:
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Event catalog
Maps event names to display properties: a human-readable title, an emoji icon, and a description function that receives the event struct. The catalog is used by the event feed, the dashboard charts, and the per-user analytics. Unknown events get a nil fallback so the UI always renders gracefully.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Event schema
A minimal schema with a
@name_format
regex that enforces lowercase dotted names like
user.signed_up
. The
metadata
field defaults to an empty map and stores arbitrary key-value data about the event. The
belongs_to :user
association links events to the user who triggered them.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Handler behaviour
A simple behaviour with a single
handle_event/1
callback. Implement this to react to events, for example sending a welcome email when a user signs up. Handlers run synchronously after insert, keeping the architecture simple while allowing extension.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Admin event queries
All read queries and analytics live in the admin layer since they're only used by admin views.
list_latest/1
powers the event feed and the per-user event list with optional name, user, and single-day date filters. Events are preloaded with their user association and ordered newest-first. The date filter compares against
?::date
so a heatmap day maps directly onto a UTC day of events.
aggregate/3
computes a count for a date range and its previous period of the same length, returning the current count, previous count, and percentage change. This drives the stat cards on the dashboard.
bar_chart/4
wraps aggregate for daily data and adds a monthly mode using
date_trunc('month', ...)
. Returns data shaped for the
<.bar_chart>
component.
stacked_bar_chart/3
runs a single grouped query for multiple event names and zero-fills each series. Uses catalog titles as dataset labels.
pie_chart/3
groups counts by event name, optionally within a date range, shaped for the
<.pie_chart>
component.
funnel_chart/3
counts distinct users per event and intersects the sets at each step, so each step shows users who completed that step
and
all previous steps.
activity_by_day/3
returns a
%{Date => count}
map of one user's events over a date range, powering the activity heatmap.
counts_by_name/2
returns a user's all-time event counts by name, descending — the source for the Top Events card.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
|
Chart components
A set of function components for rendering analytics charts, imported into all admin views through
html_helpers
in
my_app_admin_web.ex
.
stat/1
renders a card with a label, value, and color-coded percentage change indicator.
bar_chart/1
renders a horizontal bar chart with CSS-based bars (no JS charting library). Each bar's height is computed as a percentage of the max value. The
Bar
phx-hook handles tooltip positioning on hover.
stacked_bar_chart/1
extends the bar chart for multiple series stacked per day. Colors are auto-assigned from a shared
@series_colors
palette with a legend.
pie_chart/1
renders a pie or donut (
style={:donut}
) with an HTML side legend showing counts and percentages. Arc paths are precomputed in Elixir, with a full-circle special case when a single slice makes up the whole total (an SVG arc from 0 to 2π is degenerate). Donuts show the total in the center.
funnel_chart/1
renders horizontal bars that taper from 100% down based on conversion rates. Shows counts and percentages at each step.
activity_heatmap/1
renders the contribution-style day grid: one column per week, cells colored in five intensity steps by event count. Cells scale fluidly with the card width up to a 16px cap, so the graph fills wide cards and shrinks to fit phones without scrolling. An optional
day_click
attribute makes cells clickable — the event is pushed with the cell's ISO date — and
selected
highlights the active day, letting views use the heatmap as a date filter.
All chart types share the public
chart_card
wrapper for consistent titles, icons, and subtitle bars — views can also use it directly for non-chart panels, as the Top Events and User Events cards do — plus a
bar_tooltip
component for hover states.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Responsive page headers
The shared
header
component stacks its title and actions vertically on small screens instead of forcing them into one row. Together with this, the search inputs on the Users and Admin Users index pages go full-width on mobile (
w-full sm:w-64
), keeping the admin usable on phones.
|
|
|
|
|
|
|
|
| - |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Sidebar entry
An Events item is added to the App section of the admin sidebar, using the same signal icon the feed uses for uncataloged events.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| - |
|
| + |
|
|
|
|
|
|
Dashboard
The admin landing page renders four stat cards (30d, 90d, 6mo, 1yr registrations), two bar charts (daily and quarterly), a stacked bar chart comparing sign-ups vs confirmations, a registration funnel, and a pair of pie/donut charts breaking events down by type. All data is computed on mount from the query module.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
| - |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
Event feed
A real-time event feed using LiveView streams. New events arrive via PubSub subscription and are prepended to the stream. URL-based filters for event name and user ID are applied both to the initial query and to incoming broadcasts via
matches_filter?/2
. Filter pills with clear buttons make it easy to drill into specific events or users.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Event detail
Displays the event's catalog title, icon, description, user email, IDs, and metadata key-value pairs. Each property is a click-to-copy button using the
CopyToClipboard
phx-hook with a
data-copied
attribute for visual feedback.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
| - |
|
| + |
|
|
|
|
|
|
User analytics
The user show page becomes a per-user analytics view with three cards built from the event stream, all sharing the
chart_card
frame:
Activity
— a GitHub-style contribution heatmap of the user's last 26 weeks. Clicking a day cell pushes
select_day
, which re-queries the event list for that UTC day and highlights the cell; clicking the same cell again (or the Clear control) resets the filter.
Top Events — the user's five most frequent events of all time, each with a bar scaled proportionally against the most frequent one.
User Events — the latest 20 events scoped to this user, or every event on the selected heatmap day. The subtitle swaps between "N most recent events" and "N events on {date}" so the active filter is always visible. Each row is a link to the event detail page, and a View all link jumps to the global feed pre-filtered to this user.
The private
event_icon
component renders the catalog emoji when one exists and falls back to a generic signal icon, mirroring the global feed's treatment of uncataloged events.
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| - |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
Routes
Two routes join the existing authenticated admin
live_session
: the event feed at
/admin/events
and event detail at
/admin/events/:id
.
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Migration
Creates the
events
table with
name
,
metadata
(JSON map),
user_id
, and
organization_id
. The
updated_at
timestamp is omitted since events are append-only. Indexes on
name
,
inserted_at
,
user_id
,
organization_id
, and a composite
(name, inserted_at)
index support the admin query patterns.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
|
|
|
Tests
Covers
track/2
: event creation with name, metadata, and user, name format validation, and the metadata default.
test/my_app_admin/query/events_test.exs
exercises the analytics queries —
list_latest/1
filters and ordering,
aggregate/3
period-over-period math, chart shaping, and funnel intersection — using
test/support/fixtures/events_fixtures.ex
, which creates events through the real
track/2
path.
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
|
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
| + |
|
Install with Claude Code
Write instructions to implement this feature to your project directory in a LLM-friendly format, then have Claude take care of the rest! Requires Claude Code to be installed.
curl "https://elixir-saas.com/llms/p/admin-events.md?v=1.8.3&f=impl" > admin_events.md;curl "https://elixir-saas.com/llms/p/admin-events.md?v=1.8.3&f=test" > admin_events_tests.md;claude "Implement the feature that is documented in: admin_events.md, then add the tests documented in: admin_events_tests.md." --allowedTools "Write Edit Bash(mix:*) Bash(mkdir:*)";