{% if render_mode != 'fragment' %} {% if domain %}{% endif %} User Activity - Finite State Report {# ECharts library — replaces Chart.js v4.4.1 from the pre-migration template. Three chart panels: line (daily users), pie (activity by type), bar (top users by activity). #} {% include "_design_system.html" %} {% include "_console_shell.html" %} {% include "_echarts_ready.html" %} {% endif %} {# Recipe-local styles — render in BOTH standalone and fragment modes so fragment_extractor (_STYLE_RE) collects them, scopes under .fs-section-user-activity, and re-emits at the top of the fragment. Pattern established by PR #80 round-1 multi-review fix. #} {% if render_mode != 'fragment' %} {% endif %} {% import "_console_macros.html" as fs with context %} {# Defensive defaults — keep the template renderable when the upstream transform short-circuited. Mirrors license_report.html:80-87. #} {% set summary = summary | default({}) %} {% if not summary is mapping %}{% set summary = {} %}{% endif %} {% set daily_logins = daily_logins | default([]) %} {% set activity_by_type = activity_by_type | default([]) %} {% set top_users = top_users | default([]) %} {% set data = data | default([]) %} {% if render_mode != 'fragment' %} {{ fs.topbar( crumbs=["Finite State", "User Activity"], meta=[ {"label": "Domain", "value": domain or "—"}, {"label": "Period", "value": (start_date or "—") ~ " → " ~ (end_date or "—")}, {"label": "Generated", "value": generated_at or "—"}, ], controls=[], ) }} {% endif %}

User Activity

User login + audit-trail activity for the reporting period.

{{ fs.kpi_cell("Total Events", summary.get('total_events', 0)) }} {{ fs.kpi_cell("Unique Users", summary.get('unique_users', 0)) }} {{ fs.kpi_cell("Days with Activity", summary.get('active_days', 0)) }} {{ fs.kpi_cell("Avg Daily Users", "%.1f"|format(summary.get('avg_daily_users', 0) | float)) }}
{# 1. Daily active users — line chart #} {% if daily_logins and daily_logins | length > 0 %}
{{ fs.panel_head("Active users over time", meta="daily unique users + total events") }} {% if render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('daily_logins', '') | safe }}
{% else %}
{% endif %}
{% endif %} {# 2 + 3. Activity by type + Top users — two-up panels (single column main flattens to single panel each) #} {% if activity_by_type and activity_by_type | length > 0 %}
{{ fs.panel_head("Activity breakdown by type", meta=((activity_by_type | length) | string) ~ " event types") }} {% if render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('activity_by_type', '') | safe }}
{% else %}
{% endif %}
{% endif %} {% if top_users and top_users | length > 0 %}
{{ fs.panel_head("Most active users", meta="top 10 by total actions") }} {% if render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('top_users', '') | safe }}
{% else %}
{% endif %}
{# Top Users table — preserved from pre-migration template. #}
{{ fs.panel_head("Top active users", meta="ranked by total activity") }}
{% for row in top_users %} {% set actions = row.get('Total Actions', 0) %} {% set chip_cls = 'high' if actions >= 100 else ('mid' if actions >= 50 else ('low' if actions >= 10 else 'zero')) %} {% endfor %}
Rank User Total Actions Days Active Event Types Last Active
{{ loop.index }} {{ row['User'] }} {{ actions }} {{ row['Logins'] }} {{ row['Event Types'] }} {{ row['Last Active'] }}
{% endif %} {# Activity by type table — preserved from pre-migration template. #} {% if activity_by_type and activity_by_type | length > 0 %}
{{ fs.panel_head("Activity by event type", meta="counts + share of total") }}
{% for row in activity_by_type %} {% set et = row.get('Event Type', '') %} {% set cls = 'login' if 'Login' in et else ('create' if 'Create' in et else ('delete' if ('Delete' in et or 'Remove' in et) else ('update' if ('Update' in et or 'Edit' in et) else 'other'))) %} {% endfor %}
Event Type Count Percentage
{{ et }} {{ row['Count'] }} {{ "%.1f"|format(row['Percentage'] | float) }}%
{% endif %} {# Recent Activity table — preserved from pre-migration template:177-208. Cap at first 100 rows; CSV/XLSX export carries the full set. #}
{{ fs.panel_head("Recent activity", meta="latest events, most recent first") }}
{% if data and data | length > 0 %} {% for row in data[:100] %} {% set et = row.get('Event Type', '') %} {% set cls = 'login' if 'Login' in et else ('create' if 'Create' in et else ('delete' if ('Delete' in et or 'Remove' in et) else ('update' if ('Update' in et or 'Edit' in et) else 'other'))) %} {% endfor %}
Time User Event Type Project
{{ row['Time'] }} {{ row['User'] }} {{ et }} {{ row['Project'] if row.get('Project') else '—' }}
{% if data | length > 100 %}

Showing first 100 of {{ data | length }} events. Export to CSV/XLSX for the full set.

{% endif %} {% else %}

No activity found

No user activity matches the current criteria.

{% endif %}
{% if render_mode != 'fragment' %} {{ fs.status_bar(items=[ "Recipe: User Activity", "Events: " ~ ((summary.get('total_events', 0)) | string), generated_at | default(''), ], version='fs-report ' ~ (fs_report_version | default('dev'))) }} {% endif %} {% if render_mode != 'fragment' or fragment_scripts_enabled %} {% include '_action_buttons.html' %} {% endif %}