Hypertext Rails

Documentation

Getting Started

Communication Center

Procore / Project Groups

Analytics Database

Other Features

Compiled Template Preview — Layout Specification

This document describes the layout of the compiled template (email report image), which variables or components cause each box to be shown, and which table(s), attributes, and filters are used to fetch data for each row and column.

Grid columns: a narrow vertical label column (60px) is column 0; content columns are 1–4. "Row" and "column" below refer to content positions (row 1 = header, row 2 = morale/charts/risk, etc.).


Layout structure (box diagram)

Visual layout of the compiled template grid. Under each box: trigger = what must be in the template for that box to show.

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ ROW 1: HEADER (always shown)                                                                 │
├──────────────┬─────────────────────────┬──────────────────┬────────────────────────────────┤
│ Col 1        │ Col 2                    │ Col 3            │ Col 4                          │
│ Project name │ Check-in volume          │ Current month    │ PepTalk logo                    │
│ + report type│ + change %               │ (e.g. March 2026)│                                │
│              │                          │                  │                                │
│ Trigger: —   │ Trigger: —               │ Trigger: —       │ Trigger: —                      │
│ (always)     │ (always)                 │ (always)        │ (always)                        │
│ Source:      │ Source:                  │ Source:         │ Static                          │
│ project.name │ header_stats_...         │ Date.current     │                                │
└──────────────┴─────────────────────────┴──────────────────┴────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ ROW 2: MORALE + CHARTS (4 columns: label 60px | ~25% | ~50% | ~25%)                         │
├──────┬─────────────────────────┬──────────────────────────────────┬────────────────────────┤
│ Col 0│ Col 1                    │ Col 2                             │ Col 3                  │
│      │ Team Morale (big circle) │ Charts (stacked)                   │ Risk indicators        │
│ MORALE│ + optional delta        │                                    │                        │
│(label)│ Additional gauges       │                                    │                        │
│      │ (if ≥2 regular gauges)  │                                    │                        │
│      │                          │                                    │                        │
│ —    │ Trigger: ≥1 regular      │ Trigger: {{CHART:...}}             │ Trigger:               │
│      │ {{GAUGE:...}}            │ or {{BAR:...}} / {{LINE:...}}     │ {{GAUGE:...:           │
│      │ (not highest_risk_*)     │ / {{PIE:...}}                      │   highest_risk_hour    │
│      │ 1st = circle; rest below │ value_var in config (e.g. y=       │   :...}} or            │
│      │ Special: if 1st=spotcheck│ baseline_morale)                   │ {{GAUGE:...:           │
│      │ → circle uses            │                                    │   highest_risk_day:...}}│
│      │ variable_values morale   │                                    │                        │
└──────┴─────────────────────────┴──────────────────────────────────┴────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ ROW 3: INSIGHTS (4 columns: label 60px | 25% | 25% | 25% | 25%)                             │
├──────┬─────────────────────────┬─────────────────────────┬─────────────────┬────────────────┤
│ Col 0│ Col 1                    │ Col 2                    │ Col 3           │ Col 4          │
│      │ What Could Be Better     │ What Is Going Well       │ Spotcheck(s)    │ Action Tracker  │
│INSIGHT│                          │                          │                 │                │
│  S   │                          │                          │                 │                │
│      │ Trigger:                 │ Trigger:                 │ Trigger:        │ Trigger:       │
│      │ {{LIST:...:              │ {{LIST:...:              │ {{GAUGE:...:    │ {{LIST:...:    │
│      │   what_could_be_better}} │   what_is_going_well}}   │   spotcheck:...}}│   action_      │
│      │                          │                          │ Themes fill     │   tracker:...}}│
│      │                          │                          │ empty cols      │                │
│      │                          │                          │ Trigger:        │                │
│      │                          │                          │ {{THEME:...}}    │                │
└──────┴─────────────────────────┴─────────────────────────┴─────────────────┴────────────────┘

┌─────────────────────────────────────────────────────────────────────────────────────────────┐
│ ROW 4: REGULAR LISTS (only if any; 4 columns: label 60px | 25% each)                         │
├──────┬─────────────────────────┬─────────────────────────┬─────────────────┬────────────────┤
│ Col 0│ Col 1                    │ Col 2                    │ Col 3           │ Col 4          │
│      │ Regular list             │ Regular list             │ Regular list     │ Regular list   │
│LISTS │                          │                          │                 │                │
│      │ Trigger:                 │ Trigger:                 │ Trigger:        │ Trigger:       │
│      │ {{LIST:Label:value_var}} │ {{LIST:Label:value_var}} │ (same)          │ (same)         │
│      │ where value_var NOT IN   │ value_var ≠ what_could_   │                 │                │
│      │ (what_could_be_better,   │ be_better, what_is_going_│                 │                │
│      │  what_is_going_well,     │ well, action_tracker      │                 │                │
│      │  action_tracker)         │                          │                 │                │
└──────┴─────────────────────────┴─────────────────────────┴─────────────────┴────────────────┘

Plain variables (inline text only, no box in image):
{{project_name}} {{multi_project_average_morale}} {{baseline_morale}} {{this_months_morale}} {{last_months_morale}} — replaced in the intro above the compiled image.


Row 1 — Header (always shown)

Column Box / content Trigger Table(s) / attributes / filter
1 Project name + report type Always projects: name. communication_templates (via delivery): template_type. Filter: delivery.project_id, template from delivery.comms_instance.template.
2 Monthly check-in volume + change % Always check_in_submission: count where project_id = delivery project, created_at in current month range; same for previous month. Filter: project_id, created_at (beginningofmonth..endofmonth). Volume change = ((current − previous) / previous × 100).round.
3 Current month label Always Application: Date.current.strftime('%B %Y'). No table.
4 PepTalk logo Always Static. No table.

Row 2 — Morale + charts + risk

Layout: vertical label "MORALE" (column 0) then three content areas (columns 1, 2, 3).

Column Box / content Trigger Table(s) / attributes / filter
0 Vertical label "MORALE" When row 2 exists Static. No table.
1 Team Morale (big circle + delta) ≥1 regular {{GAUGE:...}} (not risk) From first morale gauge (value_var in baseline_morale, this_months_morale, multi_project_average_morale, last_months_morale). check_in_submission: project_id, created_at, score; range from gauge/config or current month. Morale via MoraleCalculationService.
1 Additional gauges (list) ≥2 regular gauges Same as gauge source per token (e.g. check_in_submission + morale calc, or other resolver per value_var).
2 Charts (bar / line / pie) {{CHART:...}} or {{BAR:...}} / {{LINE:...}} / {{PIE:...}} check_in_submission: project_id, created_at, score. Filter: project_id = delivery project; created_at in range from token (x=weekly → 3 months, x=monthly → 6 months). ChartDataService groups by day/week/month; MoraleCalculationService for y=baseline_morale / this_months_morale.
3 Risk (Highest Risk Hour / Day) {{GAUGE:...:highest_risk_hour:...}} or highest_risk_day check_in_submission: project_id, created_at, score. Filter: project_id, created_at in token date range (from_date

Row 3 — Insights (4 columns)

Layout: vertical label "INSIGHTS" (column 0) then four columns (1–4). Assignment by variable type; themes fill empty columns.

Column Box / content Trigger Table(s) / attributes / filter
0 Vertical label "INSIGHTS" When row 3 exists Static. No table.
1 What Could Be Better {{LIST:...:what_could_be_better:...}} spot_check: project_id, is_default = true (else first for project). spot_check_answer: spot_check_id, is_positive = false, id, name, answer (JSONB). check_in_submission: project_id, created_at (token date range), spot_check_id or tags (array of answer IDs). Counts per answer from tags.
2 What Is Going Well {{LIST:...:what_is_going_well:...}} Same as col 1. spot_check_answer: is_positive = true.
3 Spotcheck(s) {{GAUGE:...:spotcheck:...}} spot_check: project_id, is_scheduled = true. spot_check_answer: spot_check_id, id, name, answer. check_in_submission: project_id, created_at (token start_dateend_date), spot_check_id, tags (answer IDs). Counts per answer; question from spot_check.question.
4 Action Tracker {{LIST:...:action_tracker:...}} tasks: project_id, due_date (in token date range), status, feedback, action. Filter: project_id, due_date >= range.first, due_date <= range.last. Order: due_date ASC, created_at DESC. Limit 20.
1–4 Themes {{THEME:...}} (fill empty cols) Value/delta from resolve_variable(value_var); no dedicated theme table—uses variable name in token (e.g. custom project vars).
1–4 Other lists Other {{LIST:...}} (value_var not in the three above) Rendered in Row 4; table/attributes depend on value_var (resolver per variable).

Row 3 — How records are displayed and which filters apply (by column)

Column Component Key filters How records are displayed
1 What Could Be Better Spot check: project_id + is_default = true (else first spot check for project). Answers: is_positive = false only. Submissions: project_id, created_at in token date range (default: current month), and spot_check_id = that spot check or tags contains any of those answer IDs. Limit: from token limit=N (default 6). Ranked list of answer labels with percentage (share of total responses). Sorted by count descending. Each row: #N + label + X%. First item styled red (“first-red”), rest grey.
2 What Is Going Well Spot check: same as col 1 (is_default = true). Answers: is_positive = true only. Submissions: same as col 1 (same spot check, same date range, same tags logic). Limit: from token (default 6). Same as col 1: ranked list, label + percentage, sorted by count. First item styled green (“first-green”), rest grey.
3 Spotcheck(s) Spot check: is_scheduled = true (not is_default). One section per scheduled spot check. Submissions: project_id, created_at in token start_dateend_date, and spot_check_id in those IDs or tags present. No is_positive filter—all answers for that spot check. For each scheduled spot check: question text, then top answer (highest count) with label + percentage/meta. Multiple spotchecks each get a block (question + top answer).
4 Action Tracker Tasks: project_id, due_date in token date range (due_date >= range.first AND due_date <= range.last). Order: due_date ASC, created_at DESC. Limit: 20 (resolver); template shows first 4 lines. Optional (from token): show_status=1, show_feedback=1. Up to 4 task lines. Each line from resolver: `status

Summary of filter differences

  • Col 1 vs Col 2: Same spot check (default) and same submissions; only spot_check_answer.is_positive differs (false = “could be better”, true = “going well”). Display differs only by color (red vs green for first item).
  • Col 3 vs Col 1/2: Uses scheduled spot checks (is_scheduled = true), not default; one section per spot check; no is_positive filter; display is question + single top answer per spot check, not a single ranked list with percentages.
  • Col 4: Different source entirely—tasks by due_date; no spot check or submissions; display is task lines (status/category/action), not percentages.

Row 4 — Regular lists (dynamic)

Column Box / content Trigger Table(s) / attributes / filter
0 Vertical label "LISTS" ≥1 regular list component Static. No table.
1–4 Regular list (one per column) {{LIST:Label:value_var:...}} with value_var ∉ {whatcouldbebetter, whatisgoingwell, action_tracker} Depends on value_var (e.g. custom list vars). Resolver returns value for that variable; often list-style or feedback data from same tables as Row 3 (spot_check, spot_check_answer, check_in_submission) or other resolver-backed vars.

Summary — variables that drive which boxes appear

Box Trigger (template must contain)
Row 1 Always (header).
Row 2 Col 1 — Team Morale circle At least one regular {{GAUGE:...}} (not risk). If first regular is spotcheck, circle can use variable_values morale.
Row 2 Col 1 — Additional gauges At least two regular {{GAUGE:...}} tokens.
Row 2 Col 2 — Charts At least one {{CHART:...}} or {{BAR:...}} / {{LINE:...}} / {{PIE:...}}.
Row 2 Col 3 — Risk At least one {{GAUGE:...:highest_risk_hour:...}} or {{GAUGE:...:highest_risk_day:...}}.
Row 3 Col 1 {{LIST:...:what_could_be_better:...}}.
Row 3 Col 2 {{LIST:...:what_is_going_well:...}}.
Row 3 Col 3 {{GAUGE:...:spotcheck:...}} (spotcheck sections).
Row 3 Col 4 {{LIST:...:action_tracker:...}}.
Row 3 — themes {{THEME:...}}; fill empty insight columns.
Row 4 Any {{LIST:Label:value_var:...}} where value_var ∉ {what_could_be_better, what_is_going_well, action_tracker}.

Plain variables (inline text only)

These do not create a box in the image. They are replaced in the intro (body text above the compiled image):

  • {{project_name}}
  • {{multi_project_average_morale}}
  • {{baseline_morale}}
  • {{this_months_morale}}
  • {{last_months_morale}}
  • … any other plain {{variable_name}} (no TYPE:...)

So: row 2 column 2 in the image is the charts column. The Team Morale circle is row 2 column 1. If you said "row 2 column 2" for Team Morale, that may be 1-based counting (row 2, second content column = Team Morale).