Hypertext Rails
Documentation
Getting Started
Communication Center
- Automation Workflow Flow
- Trigger Events and Cadence Rules
- Fallback Channel Implementation
- Fallback Channel Testing (dev)
- Twilio SMS Integration Guide
- Email Tracking Setup (sent, delivered, failed, open, click)
- SMS Tracking & Twilio Free Tier
- AWS SES Delivery Tracking (console setup for delivery webhook)
- Compiled Template Guide (layout, components, variables)
- Compiled Template — Layout Spec (layout structure, triggers, data sources)
- Compiled Template — Gap Analysis (new designs vs current app)
- Workflow & Template Features (project-driven recipients, multi-project format)
Procore / Project Groups
- Procore Integration — Complete Guide (installation single/grouped, project groups, token storage, why no migration)
Analytics Database
- Analytics Database — Reference (tables, how they’re created and populated, formulas and variables for PM and developers)
- Analytics DB — Duplication Incident Report (root cause, PK / upsert safeguards)
- Analytics DB — Sync Stoppage Incident & Resilience Fix (April 6, 2026 stall; per-step rescue + guaranteed reschedule)
Other Features
- Heartbeats Dashboard (kiosk connectivity, queries, sample data)
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_date–end_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_date–end_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_positivediffers (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; nois_positivefilter; display is question + single top answer per spot check, not a single ranked list with percentages. - Col 4: Different source entirely—
tasksbydue_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}}(noTYPE:...)
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).