{% import "_console_macros.html" as fs with context %} {% if render_mode != 'fragment' %} {% if domain %}{% endif %} Platform Usage - Finite State Report {% 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 them under .fs-section-platform-usage, and re-emits at the top of the fragment. #} {% if render_mode != 'fragment' %} {% endif %} {# Defensive defaults — keep the template renderable when the transform short-circuited or a caller rendered it without a full result dict. #} {% set s = usage_summary | default({}) %} {% if not s is mapping %}{% set s = {} %}{% endif %} {% set projects = project_table | default([]) %} {% set folders = folder_table | default([]) %} {% set versions = version_table | default([]) %} {% set hygiene_rows = hygiene | default([]) %} {% set hygiene_counts = hygiene_summary | default([]) %} {% set ingestion_labels = ingestion_labels | default({}) %} {# Report-integrity rows (fetch failures, broken joins, truncated scan history) ride in hygiene_rows so CSV/XLSX readers see them too, but they describe THIS RUN, not the tenant, so they must not count toward "N flags need attention" — that number should answer "how much of this tenant's inventory needs a look", not conflate it with why the run itself may be unreliable. #} {% set hygiene_flag_count = hygiene_rows | rejectattr('entity_type', 'equalto', 'report') | list | length %} {% set notes = usage_notes | default([]) %} {% set thresholds = s.get('freshness_thresholds', {}) %} {% set scan_window = s.get('scan_window_days', 90) %} {# Same 500 the Markdown renderer caps at, so the two rendered formats truncate at the same place and reconcile against each other row for row. #} {% set version_cap = 500 %} {# Presentation-only mapping from the platform's raw enum values to the labels a reader recognises, plus the chip colour class. Deliberately NOT done in the transform: the CSV, XLSX, JSON and Markdown exports keep the raw API values (SOURCE_SCA, VULNERABILITY_ANALYSIS, UI_UPLOAD) so a machine consumer joining against the API or another report still matches, and only the rendered HTML substitutes. A value missing from these maps falls through to a neutral chip carrying the raw text, so a scan type the platform adds later shows up as itself rather than silently disappearing. #} {% set artifact_chips = { 'CONFIG': ('Config', 'config'), 'SCA': ('SCA', 'sca'), 'SAST': ('Binary SAST', 'sast'), 'FS': ('NGP', 'ngp'), 'VULNERABILITY_ANALYSIS': ('Reachability', 'reachability'), 'SBOM_IMPORT': ('SBOM', 'sbom'), 'SOURCE_SCA': ('Source SCA', 'sourcesca'), 'UNKNOWN': ('Unknown', 'other'), } %} {# Label text comes from ``ingestion_labels`` (the transform's INGESTION_LABELS, the same map the ingestion-mix chart uses) rather than being retyped here — that is the one thing that must read identically in the chart legend and this chip, and a second hand-copied literal would silently drift from it. Only the CSS class slug, a display-only concern the chart has no equivalent of, is local to this template. #} {% set ingestion_chips = { 'UI_UPLOAD': (ingestion_labels.get('UI_UPLOAD', 'GUI'), 'gui'), 'API_UPLOAD': (ingestion_labels.get('API_UPLOAD', 'API'), 'api'), 'UNKNOWN': (ingestion_labels.get('UNKNOWN', 'Unknown'), 'other'), } %} {% macro value_chips(raw, mapping) -%} {%- set text = (raw if raw is string else '') | trim -%} {%- if not text -%} — {%- else -%} {%- for token in text.split(',') -%} {%- set key = token | trim -%} {%- if key -%} {%- set mapped = mapping.get(key) -%} {{ mapped[0] if mapped else key }} {%- endif -%} {%- endfor -%} {%- endif -%} {%- endmacro %} {% if render_mode != 'fragment' %} {{ fs.topbar( crumbs=["Finite State", "Platform Usage"], meta=[ {"label": "Domain", "value": domain or "—"}, {"label": "Generated", "value": s.get('generated_at') or generated_at or "—"}, ], controls=[], ) }} {% endif %}

Platform Usage

Inventory, activity and hygiene across the organization, as of the report run time. No per-user or seat metrics — see User Activity for those.

{% if s.get('fetch_failures') %}

This report is incomplete

The {{ s.get('fetch_failures') | join(', ') }} sweep(s) failed, so every count below is a floor rather than a total and its zeros mean “not fetched”, not “none exist”. Re-run; if it persists, check the token's permissions for those endpoints.

{% endif %} {% if s.get('join_broken') %}

Every scan-derived figure below is invalid

None of the scans fetched reference any project in scope, so all of them were excluded: the scan counts, the monthly trends, the ingestion mix and every freshness value read zero or “Never scanned” regardless of the real state. Only the inventory columns — folders, projects, versions and their dates — are trustworthy here. This is a broken join, not a finding about this organization — most often a cached scan set belonging to a different account. Re-run with --refresh, or fs-report cache clear --api, and confirm the token and domain belong to the same tenant.

{% endif %} {% if s.get('scans_truncated') %}

Scan history is a floor, not a total

The scan fetch hit its page/record ceiling before reaching full history, so scan counts, freshness dates and “Never scanned” may understate reality for older or high-volume tenants — some pre-cutoff scans may be missing entirely. Narrow with --folder or --project to bring the population under the ceiling.

{% endif %} {# ── §A executive summary tiles ─────────────────────────────────── #}
{{ fs.kpi_cell("Folders", s.get('total_folders', 0)) }} {{ fs.kpi_cell( "Projects", s.get('total_projects', 0), delta=(s.get('active_projects', 0) | string) ~ " active · " ~ (s.get('archived_projects', 0) | string) ~ " archived", ) }} {{ fs.kpi_cell("Versions", s.get('total_versions', 0)) }} {{ fs.kpi_cell( "Scans · " ~ (scan_window | string) ~ "d", s.get('scans_in_window', 0), delta=(("+" if s.get('scans_window_delta', 0) > 0 else "") ~ (s.get('scans_window_delta', 0) | string) ~ " vs prior " ~ (scan_window | string) ~ "d"), delta_class=("up" if s.get('scans_window_delta', 0) > 0 else ("down" if s.get('scans_window_delta', 0) < 0 else None)), ) }} {{ fs.kpi_cell( "Active · " ~ (s.get('activity_window_days', 30) | string) ~ "d", s.get('active_recent_projects', 0), delta="of " ~ (s.get('active_projects', 0) | string) ~ " active projects", ) }} {# "Inactive", not "Stale" — the freshness bucket below is ALSO called Stale but uses a different threshold (stale_days, default 180d) over a different population (every project, not just active ones with versions). Two tiles both reading "Stale" with different day counts invited exactly the confusion this label avoids. #} {{ fs.kpi_cell( "Inactive · " ~ (s.get('stale_project_days', 90) | string) ~ "d", s.get('stale_projects', 0), dot=("high" if s.get('stale_projects', 0) > 0 else None), ) }} {{ fs.kpi_cell( "Never scanned", s.get('never_scanned_projects', 0), delta="has versions, no completed scan", dot=("critical" if s.get('never_scanned_projects', 0) > 0 else None), ) }} {{ fs.kpi_cell( "Empty projects", s.get('empty_projects', 0), delta="zero versions", dot=("medium" if s.get('empty_projects', 0) > 0 else None), ) }}
{# The disclosure block is not an appendix. Three of this report's columns stand in for platform fields that do not exist, and the tiles overlap by construction; a reader who skips this will over-read them. #} {% if notes %}
How to read these numbers — {{ notes | length }} note{{ "s" if (notes | length) != 1 }}
    {% for note in notes %}
  • {{ note }}
  • {% endfor %}
{% endif %} {# ── §E activity & trend ────────────────────────────────────────── #}
{{ fs.panel_head("Projects by freshness") }}
{% if not freshness_mix or (freshness_mix | sum(attribute='count')) == 0 %}

No projects in scope, so there is no freshness distribution to chart.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('freshness_mix', '') | safe }}
{% else %}
{% endif %}
{{ fs.panel_head("Scans per month", meta="trailing " ~ ((scans_trend | default([])) | length | string) ~ " months") }}
{% if not scans_trend %}

No scan history in scope, so there is no monthly volume to chart.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('scans_trend', '') | safe }}
{% else %}
{% endif %}
{{ fs.panel_head("New projects and versions per month") }}
{% if not growth_trend %}

No creation dates in scope, so there is no growth trend to chart.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('growth_trend', '') | safe }}
{% else %}
{% endif %}
{{ fs.panel_head("Scan volume by folder", meta="last " ~ (scan_window | string) ~ "d") }}

Includes archived projects' scans, unlike the Active tile above — this is a volume breakdown, not an engagement metric, and archived projects still have real scan history.

{% if not scans_by_folder %}

No folder recorded a scan in the last {{ scan_window }} days. Folders with zero scans in the window are omitted rather than drawn as empty bars.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('scans_by_folder', '') | safe }}
{% else %}
{% endif %}
{{ fs.panel_head("Ingestion method mix") }}
{% if not ingestion_mix %}

No scans in the trend window, so there is no ingestion mix to chart.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('ingestion_mix', '') | safe }}
{% else %}
{% endif %}
{{ fs.panel_head("Failed scan rate by month") }}
{# Keys off failure_rate_trend, not scans_trend: a scope whose months all lack a finished scan has months to plot but no rate, and an empty axis reads as a rendering failure. #} {% if not (failure_rate_trend | default([]) | rejectattr('failure_rate_pct', 'none') | list) %}

No month in scope finished a scan, so there is no failure rate to chart. A month with no completed or failed scan has no denominator — it is a gap, not 0%.

{% elif render_mode == 'fragment' and not fragment_scripts_enabled %}
{{ server_svgs.get('failure_rate_trend', '') | safe }}
{% else %}
{% endif %}
{# ── §F hygiene ─────────────────────────────────────────────────── #}
{{ fs.panel_head("Needs attention", meta=((hygiene_flag_count) | string) ~ " flag" ~ ("s" if hygiene_flag_count != 1 else "")) }}

These explain anomalies in the counts above rather than being counts themselves. Every flag is a metadata, lifecycle or report-integrity condition, not a security finding.

{% if hygiene_rows %} {% for entry in hygiene_counts %} {% set matching = hygiene_rows | selectattr('flag', 'equalto', entry.flag) | list %} {% endfor %}
Flag Count Affected
{{ entry.flag }} {{ entry.count }} {{ (matching[:8] | map(attribute='entity') | join(', ')) }}{% if matching | length > 8 %} … and {{ (matching | length) - 8 }} more{% endif %}
{{ matching[0].detail if matching else '' }}
{% else %}

Nothing flagged

Every project in scope has a recorded creator, a folder, at least one version, and a completed scan; no folder is empty and no two project names collide.

{% endif %}
{# ── §C projects (primary detail view) ──────────────────────────── #}
{{ fs.panel_head("Projects", meta=((projects | length) | string) ~ " row" ~ ("s" if (projects | length) != 1 else "")) }}

Grouped by folder, oldest-scanned first inside each folder. Freshness buckets: Current ≤ {{ thresholds.get('current_days', 30) }}d · Aging ≤ {{ thresholds.get('aging_days', 90) }}d · Stale ≤ {{ thresholds.get('stale_days', 180) }}d · Dormant beyond that.

{% if s.get('dependency_scanned_projects', 0) %}

{{ s.get('dependency_scanned_projects', 0) }} project(s) have no scan of their own and take their last-scan date, days and freshness from their project dependencies — Source reads “dependencies” on those rows. They are assemblies: the scanning happens one edge down, so counting them as never scanned would be wrong. Their own scan columns stay at zero because the dependency has its own row here; the dependency total is in Dep. scans.

{% endif %} {% if projects %} {# Client-side filter over the rendered rows of THIS table only. The Folders, Versions and Hygiene tables are deliberately unfiltered: they have different grains, so a projects-level bucket filter has no meaning on them, and filtering them independently would let three tables disagree about the same scope. Hard scoping belongs on the CLI — --folder / --project — because it changes what was fetched and therefore what every tile above means. #}
Filter: {% for bucket in ['Current', 'Aging', 'Stale', 'Dormant', 'Never scanned'] %} {% endfor %}
{% endif %} {% include "_columns_schema.html" %}
{% if projects %} {% for row in projects %} {# Blank, not 0, when nothing ever completed — a zero here would read as "scanned today". #} {# "dependencies" means the three columns to the left were inherited from this project's dependency tree, because the project itself has never completed a scan. #} {% endfor %}
Folder Project Type Status Created by Created Versions Latest version Last scan Days Freshness Source Scans (all) Scans ({{ scan_window }}d) Dep. scans Artifact types Ingestion
{{ row.folder_path or '—' }} {{ row.project_name }} {{ row.project_type or '—' }} {{ row.status }} {{ row.created_by or '—' }} {{ row.created_date or '—' }} {{ row.version_count }} {{ row.latest_version or '—' }} {{ row.last_scan_date or '—' }}{{ row.days_since_last_scan if row.days_since_last_scan is not none else '—' }} {{ row.freshness_bucket }} {{ 'dependencies' if row.scan_source == 'dependencies' else (row.scan_source or '—') }} {{ row.scans_lifetime }} {{ row.scans_in_window }} {{ row.dependency_scans or '—' }}{% if row.dependency_projects %} ({{ row.dependency_projects }} dep){% endif %} {{ value_chips(row.artifact_types, artifact_chips) }} {{ value_chips(row.ingestion_methods, ingestion_chips) }}
{% else %}

No projects in scope

No project was returned for this scope. On a --folder or --project run, confirm the name resolves; otherwise confirm the token can see projects.

{% endif %}
{# ── §B folders ─────────────────────────────────────────────────── #}
{{ fs.panel_head("Folders", meta=((folders | length) | string) ~ " row" ~ ("s" if (folders | length) != 1 else "")) }}
{% if folders %} {% for row in folders %} {% endfor %}
Folder Parent Created by Created Projects Active Archived Versions Last scan (any project)
{{ row.folder_path or row.folder_name }} {{ row.parent_folder or '—' }} {{ row.created_by or '—' }} {{ row.created_date or '—' }} {{ row.projects }} {{ row.active_projects }} {{ row.archived_projects }} {{ row.versions }} {{ row.last_scan_date or '—' }}
{% else %}

No folders in scope.

{% endif %}
{# ── §D versions ────────────────────────────────────────────────── #}
{{ fs.panel_head("Versions", meta=((versions | length) | string) ~ " row" ~ ("s" if (versions | length) != 1 else "")) }}

One row per version, across every branch. Rescan count is the engagement signal here — a version scanned repeatedly is one somebody is iterating on.

{% if versions %} {% for row in versions[:version_cap] %} {% endfor %}
Project Version Branch Folder Created Artifact type Ingestion Latest scan status Latest scan Scans
{{ row.project_name or '—' }} {{ row.version_label or '—' }} {{ row.branch or '—' }} {{ row.folder_path or '—' }} {{ row.created_date or '—' }} {{ value_chips(row.artifact_types, artifact_chips) }} {{ value_chips(row.ingestion_methods, ingestion_chips) }} {{ row.latest_scan_status }} {{ row.latest_scan_date or '—' }} {{ row.scan_count }}
{% if versions | length > version_cap %}

Showing {{ version_cap }} of {{ versions | length }} versions. The full list is in Platform Usage_Versions.csv, the XLSX Versions sheet, and the JSON output.

{% endif %} {% else %}

No versions in scope.

{% endif %}
{% if render_mode != 'fragment' %} {{ fs.status_bar(items=[ "Recipe: Platform Usage", "Projects: " ~ (s.get('total_projects', 0) | string), "Inactive: " ~ (s.get('stale_projects', 0) | string), "Never scanned: " ~ (s.get('never_scanned_projects', 0) | string), s.get('generated_at') or generated_at | default(''), ], version='fs-report ' ~ (fs_report_version | default('dev'))) }} {% endif %} {% if render_mode != 'fragment' %} {% include '_action_buttons.html' %} {% endif %}